well, tests pass now

This commit is contained in:
2024-10-14 17:33:24 -07:00
parent 795c528754
commit a813b65535
81 changed files with 15233 additions and 6 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
rustflags = ["--cfg", "tokio_unstable"]

8
.gitignore vendored
View File

@@ -13,9 +13,5 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
tarpaulin-report.html
proptest-regressions/

2634
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

54
Cargo.toml Normal file
View File

@@ -0,0 +1,54 @@
[package]
name = "hushd"
version = "0.1.0"
edition = "2021"
authors = ["awick"]
[lib]
name = "hush"
path = "src/lib.rs"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
[dependencies]
aes = { version = "0.8.4", features = ["zeroize"] }
base64 = "0.22.1"
bcrypt-pbkdf = "0.10.0"
bytes = "1.6.0"
cipher = { version = "0.4.4", features = ["alloc", "block-padding", "rand_core", "std", "zeroize"] }
clap = { version = "4.5.7", features = ["derive"] }
console-subscriber = "0.3.0"
ctr = "0.9.2"
ed25519-dalek = "2.1.1"
elliptic-curve = { version = "0.13.8", features = ["alloc", "digest", "ecdh", "pem", "pkcs8", "sec1", "serde", "std", "hash2curve", "voprf"] }
error-stack = "0.5.0"
futures = "0.3.31"
generic-array = "0.14.7"
hexdump = "0.1.2"
hickory-proto = "0.24.1"
hickory-resolver = "0.24.1"
hostname-validator = "1.1.1"
itertools = "0.13.0"
num-bigint-dig = { version = "0.8.4", features = ["arbitrary", "i128", "zeroize", "prime", "rand"] }
num-integer = { version = "0.1.46", features = ["i128"] }
num-traits = { version = "0.2.19", features = ["i128"] }
num_enum = "0.7.2"
p256 = { version = "0.13.2", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] }
p384 = { version = "0.13.0", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] }
p521 = { version = "0.13.3", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] }
proptest = "1.5.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
sec1 = "0.7.3"
serde = { version = "1.0.203", features = ["derive"] }
tempfile = "3.12.0"
thiserror = "1.0.61"
tokio = { version = "1.38.0", features = ["full", "tracing"] }
toml = "0.8.14"
tracing = "0.1.40"
tracing-core = "0.1.32"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "tracing", "json"] }
whoami = { version = "1.5.2", default-features = false }
xdg = "2.5.2"
zeroize = "1.8.1"

1795
specifications/rfc4253.txt Normal file

File diff suppressed because it is too large Load Diff

283
specifications/rfc6668.txt Normal file
View File

@@ -0,0 +1,283 @@
Internet Engineering Task Force (IETF) D. Bider
Request for Comments: 6668 Bitvise Limited
Updates: 4253 M. Baushke
Category: Standards Track Juniper Networks, Inc.
ISSN: 2070-1721 July 2012
SHA-2 Data Integrity Verification for
the Secure Shell (SSH) Transport Layer Protocol
Abstract
This memo defines algorithm names and parameters for use in some of
the SHA-2 family of secure hash algorithms for data integrity
verification in the Secure Shell (SSH) protocol. It also updates RFC
4253 by specifying a new RECOMMENDED data integrity algorithm.
Status of This Memo
This is an Internet Standards Track document.
This document is a product of the Internet Engineering Task Force
(IETF). It represents the consensus of the IETF community. It has
received public review and has been approved for publication by the
Internet Engineering Steering Group (IESG). Further information on
Internet Standards is available in Section 2 of RFC 5741.
Information about the current status of this document, any errata,
and how to provide feedback on it may be obtained at
http://www.rfc-editor.org/info/rfc6668.
Copyright Notice
Copyright (c) 2012 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(http://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document. Code Components extracted from this document must
include Simplified BSD License text as described in Section 4.e of
the Trust Legal Provisions and are provided without warranty as
described in the Simplified BSD License.
Bider & Baushke Standards Track [Page 1]
RFC 6668 Sha2-Transport Layer Protocol July 2012
1. Overview and Rationale
The Secure Shell (SSH) [RFC4251] is a very common protocol for secure
remote login on the Internet. Currently, SSH defines data integrity
verification using SHA-1 and MD5 algorithms [RFC4253]. Due to recent
security concerns with these two algorithms ([RFC6194] and [RFC6151],
respectively), implementors and users request support for data
integrity verification using some of the SHA-2 family of secure hash
algorithms.
1.1. Requirements Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
document are to be interpreted as described in [RFC2119].
2. Data Integrity Algorithms
This memo adopts the style and conventions of [RFC4253] in specifying
how the use of new data integrity algorithms are indicated in SSH.
The following new data integrity algorithms are defined:
hmac-sha2-256 RECOMMENDED HMAC-SHA2-256
(digest length = 32 bytes,
key length = 32 bytes)
hmac-sha2-512 OPTIONAL HMAC-SHA2-512
(digest length = 64 bytes,
key length = 64 bytes)
Figure 1
The Hashed Message Authentication Code (HMAC) mechanism was
originally defined in [RFC2104] and has been updated in [RFC6151].
The SHA-2 family of secure hash algorithms is defined in
[FIPS-180-3].
Sample code for the SHA-based HMAC algorithms are available in
[RFC6234]. The variants, HMAC-SHA2-224 and HMAC-SHA2-384 algorithms,
were considered but not added to this list as they have the same
computational requirements of HMAC-SHA2-256 and HMAC-SHA2-512,
respectively, and do not seem to be much used in practice.
Bider & Baushke Standards Track [Page 2]
RFC 6668 Sha2-Transport Layer Protocol July 2012
Test vectors for use of HMAC with SHA-2 are provided in [RFC4231].
Users, implementors, and administrators may choose to put these new
MACs into the proposal ahead of the REQUIRED hmac-sha1 algorithm
defined in [RFC4253] so that they are negotiated first.
3. IANA Considerations
This document augments the MAC Algorithm Names in [RFC4253] and
[RFC4250].
IANA has updated the "Secure Shell (SSH) Protocol Parameters"
registry with the following entries:
MAC Algorithm Name Reference Note
hmac-sha2-256 RFC 6668 Section 2
hmac-sha2-512 RFC 6668 Section 2
Figure 2
4. Security Considerations
The security considerations of RFC 4253 [RFC4253] apply to this
document.
The National Institute of Standards and Technology (NIST)
publications: NIST Special Publication (SP) 800-107 [800-107] and
NIST SP 800-131A [800-131A] suggest that HMAC-SHA1 and HMAC-SHA2-256
have a security strength of 128 bits and 256 bits, respectively,
which are considered acceptable key lengths.
Many users seem to be interested in the perceived safety of using the
SHA2-based algorithms for hashing.
5. References
5.1. Normative References
[FIPS-180-3]
National Institute of Standards and Technology (NIST),
United States of America, "Secure Hash Standard (SHS)",
FIPS PUB 180-3, October 2008, <http://csrc.nist.gov/
publications/fips/fips180-3/fips180-3_final.pdf>.
[RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-
Hashing for Message Authentication", RFC 2104, February
1997.
Bider & Baushke Standards Track [Page 3]
RFC 6668 Sha2-Transport Layer Protocol July 2012
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119, March 1997.
[RFC4231] Nystrom, M., "Identifiers and Test Vectors for HMAC-
SHA-224, HMAC-SHA-256, HMAC-SHA-384, and HMAC-SHA-512",
RFC 4231, December 2005.
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Transport Layer Protocol", RFC 4253, January 2006.
5.2. Informative References
[800-107] National Institute of Standards and Technology (NIST),
"Recommendation for Applications Using Approved Hash
Algorithms", NIST Special Publication 800-107, February
2009, <http://csrc.nist.gov/publications/
nistpubs/800-107/NIST-SP-800-107.pdf>.
[800-131A] National Institute of Standards and Technology (NIST),
"Transitions: Recommendation for the Transitioning of the
Use of Cryptographic Algorithms and Key Lengths", DRAFT
NIST Special Publication 800-131A, January 2011,
<http://csrc.nist.gov/publications/nistpubs/800-131A/
sp800-131A.pdf>.
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Assigned Numbers", RFC 4250, January 2006.
[RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Architecture", RFC 4251, January 2006.
[RFC6151] Turner, S. and L. Chen, "Updated Security Considerations
for the MD5 Message-Digest and the HMAC-MD5 Algorithms",
RFC 6151, March 2011.
[RFC6194] Polk, T., Chen, L., Turner, S., and P. Hoffman, "Security
Considerations for the SHA-0 and SHA-1 Message-Digest
Algorithms", RFC 6194, March 2011.
[RFC6234] Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms
(SHA and SHA-based HMAC and HKDF)", RFC 6234, May 2011.
Bider & Baushke Standards Track [Page 4]
RFC 6668 Sha2-Transport Layer Protocol July 2012
Authors' Addresses
Denis Bider
Bitvise Limited
Suites 41/42, Victoria House
26 Main Street
GI
Phone: +1 869 762 1410
EMail: ietf-ssh2@denisbider.com
URI: http://www.bitvise.com/
Mark D. Baushke
Juniper Networks, Inc.
1194 N Mathilda Av
Sunnyvale, CA 94089-1206
US
Phone: +1 408 745 2952
EMail: mdb@juniper.net
URI: http://www.juniper.net/
Bider & Baushke Standards Track [Page 5]

787
specifications/rfc8308.txt Normal file
View File

@@ -0,0 +1,787 @@
Internet Engineering Task Force (IETF) D. Bider
Request for Comments: 8308 Bitvise Limited
Updates: 4251, 4252, 4253, 4254 March 2018
Category: Standards Track
ISSN: 2070-1721
Extension Negotiation in the Secure Shell (SSH) Protocol
Abstract
This memo updates RFCs 4251, 4252, 4253, and 4254 by defining a
mechanism for Secure Shell (SSH) clients and servers to exchange
information about supported protocol extensions confidentially after
SSH key exchange.
Status of This Memo
This is an Internet Standards Track document.
This document is a product of the Internet Engineering Task Force
(IETF). It represents the consensus of the IETF community. It has
received public review and has been approved for publication by the
Internet Engineering Steering Group (IESG). Further information on
Internet Standards is available in Section 2 of RFC 7841.
Information about the current status of this document, any errata,
and how to provide feedback on it may be obtained at
https://www.rfc-editor.org/info/rfc8308.
Copyright Notice
Copyright (c) 2018 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document. Code Components extracted from this document must
include Simplified BSD License text as described in Section 4.e of
the Trust Legal Provisions and are provided without warranty as
described in the Simplified BSD License.
Bider Standards Track [Page 1]
RFC 8308 Extension Negotiation in SSH March 2018
Table of Contents
1. Overview and Rationale ..........................................3
1.1. Requirements Terminology ...................................3
1.2. Wire Encoding Terminology ..................................3
2. Extension Negotiation Mechanism .................................3
2.1. Signaling of Extension Negotiation in SSH_MSG_KEXINIT ......3
2.2. Enabling Criteria ..........................................4
2.3. SSH_MSG_EXT_INFO Message ...................................4
2.4. Message Order ..............................................5
2.5. Interpretation of Extension Names and Values ...............6
3. Initially Defined Extensions ....................................6
3.1. "server-sig-algs" ..........................................6
3.2. "delay-compression" ........................................7
3.2.1. Awkwardly Timed Key Re-Exchange .....................9
3.2.2. Subsequent Re-Exchange ..............................9
3.2.3. Compatibility Note: OpenSSH up to Version 7.5 .......9
3.3. "no-flow-control" .........................................10
3.3.1. Prior "No Flow Control" Practice ...................10
3.4. "elevation" ...............................................11
4. IANA Considerations ............................................12
4.1. Additions to Existing Registries ..........................12
4.2. New Registry: Extension Names .............................12
4.2.1. Future Assignments to Extension Names Registry .....12
5. Security Considerations ........................................12
6. References .....................................................13
6.1. Normative References ......................................13
6.2. Informative References ....................................13
Acknowledgments ...................................................14
Author's Address ..................................................14
Bider Standards Track [Page 2]
RFC 8308 Extension Negotiation in SSH March 2018
1. Overview and Rationale
Secure Shell (SSH) is a common protocol for secure communication on
the Internet. The original design of the SSH transport layer
[RFC4253] lacks proper extension negotiation. Meanwhile, diverse
implementations take steps to ensure that known message types contain
no unrecognized information. This makes it difficult for
implementations to signal capabilities and negotiate extensions
without risking disconnection. This obstacle has been recognized in
the process of updating SSH to support RSA signatures using SHA-256
and SHA-512 [RFC8332]. To avoid trial and error as well as
authentication penalties, a client must be able to discover public
key algorithms a server accepts. This extension mechanism permits
this discovery.
This memo updates RFCs 4251, 4252, 4253, and 4254.
1.1. Requirements Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
capitals, as shown here.
1.2. Wire Encoding Terminology
The wire encoding types in this document -- "byte", "uint32",
"string", "boolean", "name-list" -- have meanings as described in
[RFC4251].
2. Extension Negotiation Mechanism
2.1. Signaling of Extension Negotiation in SSH_MSG_KEXINIT
Applications implementing this mechanism MUST add one of the
following indicator names to the field kex_algorithms in the
SSH_MSG_KEXINIT message sent by the application in the first key
exchange:
o When acting as server: "ext-info-s"
o When acting as client: "ext-info-c"
The indicator name is added without quotes and MAY be added at any
position in the name-list, subject to proper separation from other
names as per name-list conventions.
Bider Standards Track [Page 3]
RFC 8308 Extension Negotiation in SSH March 2018
The names are added to the kex_algorithms field because this is one
of two name-list fields in SSH_MSG_KEXINIT that do not have a
separate copy for each data direction.
The indicator names inserted by the client and server are different
to ensure these names will not produce a match and therefore not
affect the algorithm chosen in key exchange algorithm negotiation.
The inclusion of textual indicator names is intended to provide a
clue for implementers to discover this mechanism.
2.2. Enabling Criteria
If a client or server offers "ext-info-c" or "ext-info-s"
respectively, it MUST be prepared to accept an SSH_MSG_EXT_INFO
message from the peer.
A server only needs to send "ext-info-s" if it intends to process
SSH_MSG_EXT_INFO from the client. A client only needs to send
"ext-info-c" if it plans to process SSH_MSG_EXT_INFO from the server.
If a server receives an "ext-info-c", or a client receives an
"ext-info-s", it MAY send an SSH_MSG_EXT_INFO message but is not
required to do so.
Neither party needs to wait for the other's SSH_MSG_KEXINIT in order
to decide whether to send the appropriate indicator in its own
SSH_MSG_KEXINIT.
Implementations MUST NOT send an incorrect indicator name for their
role. Implementations MAY disconnect if the counterparty sends an
incorrect indicator. If "ext-info-c" or "ext-info-s" ends up being
negotiated as a key exchange method, the parties MUST disconnect.
2.3. SSH_MSG_EXT_INFO Message
A party that received the "ext-info-c" or "ext-info-s" indicator MAY
send the following message:
byte SSH_MSG_EXT_INFO (value 7)
uint32 nr-extensions
repeat the following 2 fields "nr-extensions" times:
string extension-name
string extension-value (binary)
Bider Standards Track [Page 4]
RFC 8308 Extension Negotiation in SSH March 2018
Implementers should pay careful attention to Section 2.5, in
particular to the requirement to tolerate any sequence of bytes
(including null bytes at any position) in an unknown extension's
extension-value.
2.4. Message Order
If a client sends SSH_MSG_EXT_INFO, it MUST send it as the next
packet following the client's first SSH_MSG_NEWKEYS message to the
server.
If a server sends SSH_MSG_EXT_INFO, it MAY send it at zero, one, or
both of the following opportunities:
o As the next packet following the server's first SSH_MSG_NEWKEYS.
Where clients need information in the server's SSH_MSG_EXT_INFO to
authenticate, it is helpful if the server sends its
SSH_MSG_EXT_INFO not only as the next packet after
SSH_MSG_NEWKEYS, but without delay.
Clients cannot rely on this because the server is not required to
send the message at this time; if sent, it may be delayed by the
network. However, if a timely SSH_MSG_EXT_INFO is received, a
client can pipeline an authentication request after its
SSH_MSG_SERVICE_REQUEST, even when it needs extension information.
o Immediately preceding the server's SSH_MSG_USERAUTH_SUCCESS, as
defined in [RFC4252].
The server MAY send SSH_MSG_EXT_INFO at this second opportunity,
whether or not it sent it at the first. A client that sent
"ext-info-c" MUST accept a server's SSH_MSG_EXT_INFO at both
opportunities but MUST NOT require it.
This allows a server to reveal support for additional extensions
that it was unwilling to reveal to an unauthenticated client. If
a server sends a second SSH_MSG_EXT_INFO, this replaces any
initial one, and both the client and the server re-evaluate
extensions in effect. The server's second SSH_MSG_EXT_INFO is
matched against the client's original.
The timing of the second opportunity is chosen for the following
reasons. If the message was sent earlier, it would not allow the
server to withhold information until the client has authenticated.
If it was sent later, a client that needs information from the
second SSH_MSG_EXT_INFO immediately after it authenticates would
have no way to reliably know whether to expect the message.
Bider Standards Track [Page 5]
RFC 8308 Extension Negotiation in SSH March 2018
2.5. Interpretation of Extension Names and Values
Each extension is identified by its extension-name and defines the
conditions under which the extension is considered to be in effect.
Applications MUST ignore unrecognized extension-names.
When it is specified, an extension MAY dictate that, in order to take
effect, both parties must include it in their SSH_MSG_EXT_INFO or
that it is sufficient for only one party to include it. However,
other rules MAY be specified. The relative order in which extensions
appear in an SSH_MSG_EXT_INFO message MUST be ignored.
Extension-value fields are interpreted as defined by their respective
extension. This field MAY be empty if permitted by the extension.
Applications that do not implement or recognize an extension MUST
ignore its extension-value, regardless of its size or content.
Applications MUST tolerate any sequence of bytes -- including null
bytes at any position -- in an unknown extension's extension-value.
The cumulative size of an SSH_MSG_EXT_INFO message is limited only by
the maximum packet length that an implementation may apply in
accordance with [RFC4253]. Implementations MUST accept well-formed
SSH_MSG_EXT_INFO messages up to the maximum packet length they
accept.
3. Initially Defined Extensions
3.1. "server-sig-algs"
This extension is sent with the following extension name and value:
string "server-sig-algs"
name-list public-key-algorithms-accepted
The name-list type is a strict subset of the string type and is thus
permissible as an extension-value. See [RFC4251] for more
information.
This extension is sent by the server and contains a list of public
key algorithms that the server is able to process as part of a
"publickey" authentication request. If a client sends this
extension, the server MAY ignore it and MAY disconnect.
In this extension, a server MUST enumerate all public key algorithms
it might accept during user authentication. However, early server
implementations that do not enumerate all accepted algorithms do
Bider Standards Track [Page 6]
RFC 8308 Extension Negotiation in SSH March 2018
exist. For this reason, a client MAY send a user authentication
request using a public key algorithm not included in "server-sig-
algs".
A client that wishes to proceed with public key authentication MAY
wait for the server's SSH_MSG_EXT_INFO so it can send a "publickey"
authentication request with an appropriate public key algorithm,
rather than resorting to trial and error.
Servers that implement public key authentication SHOULD implement
this extension.
If a server does not send this extension, a client MUST NOT make any
assumptions about the server's public key algorithm support, and MAY
proceed with authentication requests using trial and error. Note
that implementations are known to exist that apply authentication
penalties if the client attempts to use an unexpected public key
algorithm.
Authentication penalties are applied by servers to deter brute-force
password guessing, username enumeration, and other types of behavior
deemed suspicious by server administrators or implementers.
Penalties may include automatic IP address throttling or blocking,
and they may trigger email alerts or auditing.
3.2. "delay-compression"
This extension MAY be sent by both parties as follows:
string "delay-compression"
string:
name-list compression_algorithms_client_to_server
name-list compression_algorithms_server_to_client
The extension-value is a string that encodes two name-lists. The
name-lists themselves have the encoding of strings. For example, to
indicate a preference for algorithms "foo,bar" in the client-to-
server direction and "bar,baz" in the server-to-client direction, a
sender encodes the extension-value as follows (including its length):
00000016 00000007 666f6f2c626172 00000007 6261722c62617a
This same encoding could be sent by either party -- client or server.
This extension allows the server and client to renegotiate
compression algorithm support without having to conduct a key
re-exchange, which puts new algorithms into effect immediately upon
successful authentication.
Bider Standards Track [Page 7]
RFC 8308 Extension Negotiation in SSH March 2018
This extension takes effect only if both parties send it. Name-lists
MAY include any compression algorithm that could have been negotiated
in SSH_MSG_KEXINIT, except algorithms that define their own delayed
compression semantics. This means "zlib,none" is a valid algorithm
list in this context, but "zlib@openssh.com" is not.
If both parties send this extension, but the name-lists do not
contain a common algorithm in either direction, the parties MUST
disconnect in the same way as if negotiation failed as part of
SSH_MSG_KEXINIT.
If this extension takes effect, the renegotiated compression
algorithm is activated for the very next SSH message after the
trigger message:
o Sent by the server, the trigger message is
SSH_MSG_USERAUTH_SUCCESS.
o Sent by the client, the trigger message is SSH_MSG_NEWCOMPRESS.
If this extension takes effect, the client MUST send the following
message within a reasonable number of outgoing SSH messages after
receiving SSH_MSG_USERAUTH_SUCCESS, but not necessarily as the first
such outgoing message:
byte SSH_MSG_NEWCOMPRESS (value 8)
The purpose of SSH_MSG_NEWCOMPRESS is to avoid a race condition where
the server cannot reliably know whether a message sent by the client
was sent before or after receiving the server's
SSH_MSG_USERAUTH_SUCCESS. For example, clients may send keep-alive
messages during logon processing.
As is the case for all extensions unless otherwise noted, the server
MAY delay including this extension until its secondary
SSH_MSG_EXT_INFO, sent before SSH_MSG_USERAUTH_SUCCESS. This allows
the server to avoid advertising compression until the client has
authenticated.
If the parties renegotiate compression using this extension in a
session where compression is already enabled and the renegotiated
algorithm is the same in one or both directions, then the internal
compression state MUST be reset for each direction at the time the
renegotiated algorithm takes effect.
Bider Standards Track [Page 8]
RFC 8308 Extension Negotiation in SSH March 2018
3.2.1. Awkwardly Timed Key Re-Exchange
A party that has signaled, or intends to signal, support for this
extension in an SSH session MUST NOT initiate key re-exchange in that
session until either of the following occurs:
o This extension was negotiated, and the party that's about to start
key re-exchange already sent its trigger message for compression.
o The party has sent (if server) or received (if client) the message
SSH_MSG_USERAUTH_SUCCESS, and this extension was not negotiated.
If a party violates this rule, the other party MAY disconnect.
In general, parties SHOULD NOT start key re-exchange before
successful user authentication but MAY tolerate it if not using this
extension.
3.2.2. Subsequent Re-Exchange
In subsequent key re-exchanges that unambiguously begin after the
compression trigger messages, the compression algorithms negotiated
in re-exchange override the algorithms negotiated with this
extension.
3.2.3. Compatibility Note: OpenSSH up to Version 7.5
This extension uses a binary extension-value encoding. OpenSSH
clients up to and including version 7.5 advertise support to receive
SSH_MSG_EXT_INFO but disconnect on receipt of an extension-value
containing null bytes. This is an error fixed in OpenSSH
version 7.6.
Implementations that wish to interoperate with OpenSSH 7.5 and
earlier are advised to check the remote party's SSH version string
and omit this extension if an affected version is detected. Affected
versions do not implement this extension, so there is no harm in
omitting it. The extension SHOULD NOT be omitted if the detected
OpenSSH version is 7.6 or higher. This would make it harder for the
OpenSSH project to implement this extension in a higher version.
Bider Standards Track [Page 9]
RFC 8308 Extension Negotiation in SSH March 2018
3.3. "no-flow-control"
This extension is sent with the following extension name and value:
string "no-flow-control"
string choice of: "p" for preferred | "s" for supported
A party SHOULD send "s" if it supports "no-flow-control" but does not
prefer to enable it. A party SHOULD send "p" if it prefers to enable
the extension if the other party supports it. Parties MAY disconnect
if they receive a different extension value.
For this extension to take effect, the following must occur:
o This extension MUST be sent by both parties.
o At least one party MUST have sent the value "p" (preferred).
If this extension takes effect, the "initial window size" fields in
SSH_MSG_CHANNEL_OPEN and SSH_MSG_CHANNEL_OPEN_CONFIRMATION, as
defined in [RFC4254], become meaningless. The values of these fields
MUST be ignored, and a channel behaves as if all window sizes are
infinite. Neither side is required to send any
SSH_MSG_CHANNEL_WINDOW_ADJUST messages, and if received, such
messages MUST be ignored.
This extension is intended for, but not limited to, use by file
transfer applications that are only going to use one channel and for
which the flow control provided by SSH is an impediment, rather than
a feature.
Implementations MUST refuse to open more than one simultaneous
channel when this extension is in effect. Nevertheless, server
implementations SHOULD support clients opening more than one
non-simultaneous channel.
3.3.1. Prior "No Flow Control" Practice
Before this extension, some applications would simply not implement
SSH flow control, sending an initial channel window size of 2^32 - 1.
Applications SHOULD NOT do this for the following reasons:
o It is plausible to transfer more than 2^32 bytes over a channel.
Such a channel will hang if the other party implements SSH flow
control according to [RFC4254].
Bider Standards Track [Page 10]
RFC 8308 Extension Negotiation in SSH March 2018
o Implementations that cannot handle large channel window sizes
exist, and they can exhibit non-graceful behaviors, including
disconnect.
3.4. "elevation"
The terms "elevation" and "elevated" refer to an operating system
mechanism where an administrator's logon session is associated with
two security contexts: one limited and one with administrative
rights. To "elevate" such a session is to activate the security
context with full administrative rights. For more information about
this mechanism on Windows, see [WINADMIN] and [WINTOKEN].
This extension MAY be sent by the client as follows:
string "elevation"
string choice of: "y" | "n" | "d"
A client sends "y" to indicate its preference that the session should
be elevated; "n" to not be elevated; and "d" for the server to use
its default behavior. The server MAY disconnect if it receives a
different extension value. If a client does not send the "elevation"
extension, the server SHOULD act as if "d" was sent.
If a client has included this extension, then after authentication, a
server that supports this extension SHOULD indicate to the client
whether elevation was done by sending the following global request:
byte SSH_MSG_GLOBAL_REQUEST
string "elevation"
boolean want reply = false
boolean elevation performed
Clients that implement this extension help reduce attack surface for
Windows servers that handle administrative logins. Where clients do
not support this extension, servers must elevate sessions to allow
full access by administrative users always. Where clients support
this extension, sessions can be created without elevation unless
requested.
Bider Standards Track [Page 11]
RFC 8308 Extension Negotiation in SSH March 2018
4. IANA Considerations
4.1. Additions to Existing Registries
IANA has added the following entries to the "Message Numbers"
registry [IANA-M] under the "Secure Shell (SSH) Protocol Parameters"
registry [RFC4250]:
Value Message ID Reference
-----------------------------------------
7 SSH_MSG_EXT_INFO RFC 8308
8 SSH_MSG_NEWCOMPRESS RFC 8308
IANA has also added the following entries to the "Key Exchange Method
Names" registry [IANA-KE]:
Method Name Reference Note
------------------------------------------
ext-info-s RFC 8308 Section 2
ext-info-c RFC 8308 Section 2
4.2. New Registry: Extension Names
Also under the "Secure Shell (SSH) Protocol Parameters" registry,
IANA has created a new "Extension Names" registry, with the following
initial content:
Extension Name Reference Note
------------------------------------------------
server-sig-algs RFC 8308 Section 3.1
delay-compression RFC 8308 Section 3.2
no-flow-control RFC 8308 Section 3.3
elevation RFC 8308 Section 3.4
4.2.1. Future Assignments to Extension Names Registry
Names in the "Extension Names" registry MUST follow the conventions
for names defined in [RFC4250], Section 4.6.1.
Requests for assignments of new non-local names in the "Extension
Names" registry (i.e., names not including the '@' character) MUST be
done using the IETF Review policy, as described in [RFC8126].
5. Security Considerations
Security considerations are discussed throughout this document. This
document updates the SSH protocol as defined in [RFC4251] and related
documents. The security considerations of [RFC4251] apply.
Bider Standards Track [Page 12]
RFC 8308 Extension Negotiation in SSH March 2018
6. References
6.1. Normative References
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119,
DOI 10.17487/RFC2119, March 1997,
<https://www.rfc-editor.org/info/rfc2119>.
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Assigned Numbers", RFC 4250,
DOI 10.17487/RFC4250, January 2006,
<https://www.rfc-editor.org/info/rfc4250>.
[RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251,
January 2006, <https://www.rfc-editor.org/info/rfc4251>.
[RFC4252] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Authentication Protocol", RFC 4252, DOI 10.17487/RFC4252,
January 2006, <https://www.rfc-editor.org/info/rfc4252>.
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253,
January 2006, <https://www.rfc-editor.org/info/rfc4253>.
[RFC4254] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Connection Protocol", RFC 4254, DOI 10.17487/RFC4254,
January 2006, <https://www.rfc-editor.org/info/rfc4254>.
[RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for
Writing an IANA Considerations Section in RFCs", BCP 26,
RFC 8126, DOI 10.17487/RFC8126, June 2017,
<https://www.rfc-editor.org/info/rfc8126>.
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
6.2. Informative References
[IANA-KE] IANA, "Key Exchange Method Names",
<https://www.iana.org/assignments/ssh-parameters/>.
[IANA-M] IANA, "Message Numbers",
<https://www.iana.org/assignments/ssh-parameters/>.
Bider Standards Track [Page 13]
RFC 8308 Extension Negotiation in SSH March 2018
[RFC8332] Bider, D., "Use of RSA Keys with SHA-256 and SHA-512 in
the Secure Shell (SSH) Protocol", RFC 8332,
DOI 10.17487/RFC8332, March 2018,
<https://www.rfc-editor.org/info/rfc8332>.
[WINADMIN] Microsoft, "How to launch a process as a Full
Administrator when UAC is enabled?", March 2013,
<https://blogs.msdn.microsoft.com/winsdk/2013/03/22/
how-to-launch-a-process-as-a-full-administrator-when-
uac-is-enabled/>.
[WINTOKEN] Microsoft, "TOKEN_ELEVATION_TYPE enumeration",
<https://msdn.microsoft.com/en-us/library/windows/desktop/
bb530718.aspx>.
Acknowledgments
Thanks to Markus Friedl and Damien Miller for comments and initial
implementation. Thanks to Peter Gutmann, Roumen Petrov, Mark D.
Baushke, Daniel Migault, Eric Rescorla, Matthew A. Miller, Mirja
Kuehlewind, Adam Roach, Spencer Dawkins, Alexey Melnikov, and Ben
Campbell for reviews and feedback.
Author's Address
Denis Bider
Bitvise Limited
4105 Lombardy Court
Colleyville, TX 76034
United States of America
Email: ietf-ssh3@denisbider.com
URI: https://www.bitvise.com/
Bider Standards Track [Page 14]

507
specifications/rfc8332.txt Normal file
View File

@@ -0,0 +1,507 @@
Internet Engineering Task Force (IETF) D. Bider
Request for Comments: 8332 Bitvise Limited
Updates: 4252, 4253 March 2018
Category: Standards Track
ISSN: 2070-1721
Use of RSA Keys with SHA-256 and SHA-512
in the Secure Shell (SSH) Protocol
Abstract
This memo updates RFCs 4252 and 4253 to define new public key
algorithms for use of RSA keys with SHA-256 and SHA-512 for server
and client authentication in SSH connections.
Status of This Memo
This is an Internet Standards Track document.
This document is a product of the Internet Engineering Task Force
(IETF). It represents the consensus of the IETF community. It has
received public review and has been approved for publication by the
Internet Engineering Steering Group (IESG). Further information on
Internet Standards is available in Section 2 of RFC 7841.
Information about the current status of this document, any errata,
and how to provide feedback on it may be obtained at
https://www.rfc-editor.org/info/rfc8332.
Bider Standards Track [Page 1]
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
Copyright Notice
Copyright (c) 2018 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document. Code Components extracted from this document must
include Simplified BSD License text as described in Section 4.e of
the Trust Legal Provisions and are provided without warranty as
described in the Simplified BSD License.
This document may contain material from IETF Documents or IETF
Contributions published or made publicly available before November
10, 2008. The person(s) controlling the copyright in some of this
material may not have granted the IETF Trust the right to allow
modifications of such material outside the IETF Standards Process.
Without obtaining an adequate license from the person(s) controlling
the copyright in such materials, this document may not be modified
outside the IETF Standards Process, and derivative works of it may
not be created outside the IETF Standards Process, except to format
it for publication as an RFC or to translate it into languages other
than English.
Table of Contents
1. Overview and Rationale . . . . . . . . . . . . . . . . . . . 3
1.1. Requirements Terminology . . . . . . . . . . . . . . . . 3
1.2. Wire Encoding Terminology . . . . . . . . . . . . . . . . 3
2. Public Key Format vs. Public Key Algorithm . . . . . . . . . 3
3. New RSA Public Key Algorithms . . . . . . . . . . . . . . . . 4
3.1. Use for Server Authentication . . . . . . . . . . . . . . 5
3.2. Use for Client Authentication . . . . . . . . . . . . . . 5
3.3. Discovery of Public Key Algorithms Supported by Servers . 6
4. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 6
5. Security Considerations . . . . . . . . . . . . . . . . . . . 7
5.1. Key Size and Signature Hash . . . . . . . . . . . . . . . 7
5.2. Transition . . . . . . . . . . . . . . . . . . . . . . . 7
5.3. PKCS #1 v1.5 Padding and Signature Verification . . . . . 7
6. References . . . . . . . . . . . . . . . . . . . . . . . . . 8
6.1. Normative References . . . . . . . . . . . . . . . . . . 8
6.2. Informative References . . . . . . . . . . . . . . . . . 8
Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 9
Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 9
Bider Standards Track [Page 2]
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
1. Overview and Rationale
Secure Shell (SSH) is a common protocol for secure communication on
the Internet. In [RFC4253], SSH originally defined the public key
algorithms "ssh-rsa" for server and client authentication using RSA
with SHA-1, and "ssh-dss" using 1024-bit DSA and SHA-1. These
algorithms are now considered deficient. For US government use, NIST
has disallowed 1024-bit RSA and DSA, and use of SHA-1 for signing
[NIST.800-131A].
This memo updates RFCs 4252 and 4253 to define new public key
algorithms allowing for interoperable use of existing and new RSA
keys with SHA-256 and SHA-512.
1.1. Requirements Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
capitals, as shown here.
1.2. Wire Encoding Terminology
The wire encoding types in this document -- "boolean", "byte",
"string", "mpint" -- have meanings as described in [RFC4251].
2. Public Key Format vs. Public Key Algorithm
In [RFC4252], the concept "public key algorithm" is used to establish
a relationship between one algorithm name, and:
A. procedures used to generate and validate a private/public
keypair;
B. a format used to encode a public key; and
C. procedures used to calculate, encode, and verify a signature.
This document uses the term "public key format" to identify only A
and B in isolation. The term "public key algorithm" continues to
identify all three aspects -- A, B, and C.
Bider Standards Track [Page 3]
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
3. New RSA Public Key Algorithms
This memo adopts the style and conventions of [RFC4253] in specifying
how use of a public key algorithm is indicated in SSH.
The following new public key algorithms are defined:
rsa-sha2-256 RECOMMENDED sign Raw RSA key
rsa-sha2-512 OPTIONAL sign Raw RSA key
These algorithms are suitable for use both in the SSH transport layer
[RFC4253] for server authentication and in the authentication layer
[RFC4252] for client authentication.
Since RSA keys are not dependent on the choice of hash function, the
new public key algorithms reuse the "ssh-rsa" public key format as
defined in [RFC4253]:
string "ssh-rsa"
mpint e
mpint n
All aspects of the "ssh-rsa" format are kept, including the encoded
string "ssh-rsa". This allows existing RSA keys to be used with the
new public key algorithms, without requiring re-encoding or affecting
already trusted key fingerprints.
Signing and verifying using these algorithms is performed according
to the RSASSA-PKCS1-v1_5 scheme in [RFC8017] using SHA-2 [SHS] as
hash.
For the algorithm "rsa-sha2-256", the hash used is SHA-256.
For the algorithm "rsa-sha2-512", the hash used is SHA-512.
The resulting signature is encoded as follows:
string "rsa-sha2-256" / "rsa-sha2-512"
string rsa_signature_blob
The value for 'rsa_signature_blob' is encoded as a string that
contains an octet string S (which is the output of RSASSA-PKCS1-v1_5)
and that has the same length (in octets) as the RSA modulus. When S
contains leading zeros, there exist signers that will send a shorter
encoding of S that omits them. A verifier MAY accept shorter
encodings of S with one or more leading zeros omitted.
Bider Standards Track [Page 4]
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
3.1. Use for Server Authentication
To express support and preference for one or both of these algorithms
for server authentication, the SSH client or server includes one or
both algorithm names, "rsa-sha2-256" and/or "rsa-sha2-512", in the
name-list field "server_host_key_algorithms" in the SSH_MSG_KEXINIT
packet [RFC4253]. If one of the two host key algorithms is
negotiated, the server sends an "ssh-rsa" public key as part of the
negotiated key exchange method (e.g., in SSH_MSG_KEXDH_REPLY) and
encodes a signature with the appropriate signature algorithm name --
either "rsa-sha2-256" or "rsa-sha2-512".
3.2. Use for Client Authentication
To use this algorithm for client authentication, the SSH client sends
an SSH_MSG_USERAUTH_REQUEST message [RFC4252] encoding the
"publickey" method and encoding the string field "public key
algorithm name" with the value "rsa-sha2-256" or "rsa-sha2-512". The
"public key blob" field encodes the RSA public key using the
"ssh-rsa" public key format.
For example, as defined in [RFC4252] and [RFC4253], an SSH
"publickey" authentication request using an "rsa-sha2-512" signature
would be properly encoded as follows:
byte SSH_MSG_USERAUTH_REQUEST
string user name
string service name
string "publickey"
boolean TRUE
string "rsa-sha2-512"
string public key blob:
string "ssh-rsa"
mpint e
mpint n
string signature:
string "rsa-sha2-512"
string rsa_signature_blob
If the client includes the signature field, the client MUST encode
the same algorithm name in the signature as in
SSH_MSG_USERAUTH_REQUEST -- either "rsa-sha2-256" or "rsa-sha2-512".
If a server receives a mismatching request, it MAY apply arbitrary
authentication penalties, including but not limited to authentication
failure or disconnect.
Bider Standards Track [Page 5]
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
OpenSSH 7.2 (but not 7.2p2) incorrectly encodes the algorithm in the
signature as "ssh-rsa" when the algorithm in SSH_MSG_USERAUTH_REQUEST
is "rsa-sha2-256" or "rsa-sha2-512". In this case, the signature
does actually use either SHA-256 or SHA-512. A server MAY, but is
not required to, accept this variant or another variant that
corresponds to a good-faith implementation and is considered safe to
accept.
3.3. Discovery of Public Key Algorithms Supported by Servers
Implementation experience has shown that there are servers that apply
authentication penalties to clients attempting public key algorithms
that the SSH server does not support.
Servers that accept rsa-sha2-* signatures for client authentication
SHOULD implement the extension negotiation mechanism defined in
[RFC8308], including especially the "server-sig-algs" extension.
When authenticating with an RSA key against a server that does not
implement the "server-sig-algs" extension, clients MAY default to an
"ssh-rsa" signature to avoid authentication penalties. When the new
rsa-sha2-* algorithms have been sufficiently widely adopted to
warrant disabling "ssh-rsa", clients MAY default to one of the new
algorithms.
4. IANA Considerations
IANA has updated the "Secure Shell (SSH) Protocol Parameters"
registry, established with [RFC4250], to extend the table "Public Key
Algorithm Names" [IANA-PKA] as follows.
- To the immediate right of the column "Public Key Algorithm Name",
a new column has been added, titled "Public Key Format". For
existing entries, the column "Public Key Format" has been assigned
the same value as under "Public Key Algorithm Name".
- Immediately following the existing entry for "ssh-rsa", two
sibling entries have been added:
P. K. Alg. Name P. K. Format Reference Note
rsa-sha2-256 ssh-rsa RFC 8332 Section 3
rsa-sha2-512 ssh-rsa RFC 8332 Section 3
Bider Standards Track [Page 6]
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
5. Security Considerations
The security considerations of [RFC4251] apply to this document.
5.1. Key Size and Signature Hash
The National Institute of Standards and Technology (NIST) Special
Publication 800-131A, Revision 1 [NIST.800-131A] disallows RSA and
DSA keys shorter than 2048 bits for US government use. The same
document disallows the SHA-1 hash function for digital signature
generation, except under NIST's protocol-specific guidance.
It is prudent to follow this advice also outside of US government
use.
5.2. Transition
This document is based on the premise that RSA is used in
environments where a gradual, compatible transition to improved
algorithms will be better received than one that is abrupt and
incompatible. It advises that SSH implementations add support for
new RSA public key algorithms along with SSH_MSG_EXT_INFO and the
"server-sig-algs" extension to allow coexistence of new deployments
with older versions that support only "ssh-rsa". Nevertheless,
implementations SHOULD start to disable "ssh-rsa" in their default
configurations as soon as the implementers believe that new RSA
signature algorithms have been widely adopted.
5.3. PKCS #1 v1.5 Padding and Signature Verification
This document prescribes RSASSA-PKCS1-v1_5 signature padding because:
(1) RSASSA-PSS is not universally available to all implementations;
(2) PKCS #1 v1.5 is widely supported in existing SSH
implementations;
(3) PKCS #1 v1.5 is not known to be insecure for use in this scheme.
Implementers are advised that a signature with RSASSA-PKCS1-v1_5
padding MUST NOT be verified by applying the RSA key to the
signature, and then parsing the output to extract the hash. This may
give an attacker opportunities to exploit flaws in the parsing and
vary the encoding. Verifiers MUST instead apply RSASSA-PKCS1-v1_5
padding to the expected hash, then compare the encoded bytes with the
output of the RSA operation.
Bider Standards Track [Page 7]
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
6. References
6.1. Normative References
[SHS] NIST, "Secure Hash Standard (SHS)", FIPS Publication
180-4, August 2015,
<http://dx.doi.org/10.6028/NIST.FIPS.180-4>.
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119,
DOI 10.17487/RFC2119, March 1997,
<https://www.rfc-editor.org/info/rfc2119>.
[RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251,
January 2006, <https://www.rfc-editor.org/info/rfc4251>.
[RFC4252] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Authentication Protocol", RFC 4252, DOI 10.17487/RFC4252,
January 2006, <https://www.rfc-editor.org/info/rfc4252>.
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253,
January 2006, <https://www.rfc-editor.org/info/rfc4253>.
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
[RFC8308] Bider, D., "Extension Negotiation in the Secure Shell
(SSH) Protocol", RFC 8308, DOI 10.17487/RFC8308, March
2018, <https://www.rfc-editor.org/info/rfc8308>.
6.2. Informative References
[NIST.800-131A]
NIST, "Transitions: Recommendation for Transitioning the
Use of Cryptographic Algorithms and Key Lengths", NIST
Special Publication 800-131A, Revision 1,
DOI 10.6028/NIST.SP.800-131Ar1, November 2015,
<http://nvlpubs.nist.gov/nistpubs/SpecialPublications/
NIST.SP.800-131Ar1.pdf>.
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Assigned Numbers", RFC 4250,
DOI 10.17487/RFC4250, January 2006,
<https://www.rfc-editor.org/info/rfc4250>.
Bider Standards Track [Page 8]
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
[RFC8017] Moriarty, K., Ed., Kaliski, B., Jonsson, J., and A. Rusch,
"PKCS #1: RSA Cryptography Specifications Version 2.2",
RFC 8017, DOI 10.17487/RFC8017, November 2016,
<https://www.rfc-editor.org/info/rfc8017>.
[IANA-PKA]
IANA, "Secure Shell (SSH) Protocol Parameters",
<https://www.iana.org/assignments/ssh-parameters/>.
Acknowledgments
Thanks to Jon Bright, Niels Moeller, Stephen Farrell, Mark D.
Baushke, Jeffrey Hutzelman, Hanno Boeck, Peter Gutmann, Damien
Miller, Mat Berchtold, Roumen Petrov, Daniel Migault, Eric Rescorla,
Russ Housley, Alissa Cooper, Adam Roach, and Ben Campbell for
reviews, comments, and suggestions.
Author's Address
Denis Bider
Bitvise Limited
4105 Lombardy Court
Colleyville, Texas 76034
United States of America
Email: ietf-ssh3@denisbider.com
URI: https://www.bitvise.com/
Bider Standards Track [Page 9]

317
specifications/rfc8709.txt Normal file
View File

@@ -0,0 +1,317 @@
Internet Engineering Task Force (IETF) B. Harris
Request for Comments: 8709
Updates: 4253 L. Velvindron
Category: Standards Track cyberstorm.mu
ISSN: 2070-1721 February 2020
Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH)
Protocol
Abstract
This document describes the use of the Ed25519 and Ed448 digital
signature algorithms in the Secure Shell (SSH) protocol.
Accordingly, this RFC updates RFC 4253.
Status of This Memo
This is an Internet Standards Track document.
This document is a product of the Internet Engineering Task Force
(IETF). It represents the consensus of the IETF community. It has
received public review and has been approved for publication by the
Internet Engineering Steering Group (IESG). Further information on
Internet Standards is available in Section 2 of RFC 7841.
Information about the current status of this document, any errata,
and how to provide feedback on it may be obtained at
https://www.rfc-editor.org/info/rfc8709.
Copyright Notice
Copyright (c) 2020 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document. Code Components extracted from this document must
include Simplified BSD License text as described in Section 4.e of
the Trust Legal Provisions and are provided without warranty as
described in the Simplified BSD License.
Table of Contents
1. Introduction
2. Conventions Used in This Document
2.1. Requirements Language
3. Public Key Algorithm
4. Public Key Format
5. Signature Algorithm
6. Signature Format
7. Verification Algorithm
8. SSHFP DNS Resource Records
9. IANA Considerations
10. Security Considerations
11. References
11.1. Normative References
11.2. Informative References
Acknowledgements
Authors' Addresses
1. Introduction
Secure Shell (SSH) [RFC4251] is a secure remote-login protocol. It
provides for an extensible variety of public key algorithms for
identifying servers and users to one another. Ed25519 [RFC8032] is a
digital signature system. OpenSSH 6.5 [OpenSSH-6.5] introduced
support for using Ed25519 for server and user authentication and was
then followed by other SSH implementations.
This document describes the method implemented by OpenSSH and others
and formalizes the use of the name "ssh-ed25519". Additionally, this
document describes the use of Ed448 and formalizes the use of the
name "ssh-ed448".
2. Conventions Used in This Document
The descriptions of key and signature formats use the notation
introduced in [RFC4251], Section 3 and the string data type from
[RFC4251], Section 5.
2.1. Requirements Language
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
capitals, as shown here.
3. Public Key Algorithm
This document describes a public key algorithm for use with SSH, as
per [RFC4253], Section 6.6. The name of the algorithm is "ssh-
ed25519". This algorithm only supports signing and not encryption.
Additionally, this document describes another public key algorithm.
The name of the algorithm is "ssh-ed448". This algorithm only
supports signing and not encryption.
Standard implementations of SSH SHOULD implement these signature
algorithms.
4. Public Key Format
The "ssh-ed25519" key format has the following encoding:
string "ssh-ed25519"
string key
Here, 'key' is the 32-octet public key described in [RFC8032],
Section 5.1.5.
The "ssh-ed448" key format has the following encoding:
string "ssh-ed448"
string key
Here, 'key' is the 57-octet public key described in [RFC8032],
Section 5.2.5.
5. Signature Algorithm
Signatures are generated according to the procedure in Sections 5.1.6
and 5.2.6 of [RFC8032].
6. Signature Format
The "ssh-ed25519" key format has the following encoding:
string "ssh-ed25519"
string signature
Here, 'signature' is the 64-octet signature produced in accordance
with [RFC8032], Section 5.1.6.
The "ssh-ed448" key format has the following encoding:
string "ssh-ed448"
string signature
Here, 'signature' is the 114-octet signature produced in accordance
with [RFC8032], Section 5.2.6.
7. Verification Algorithm
Ed25519 signatures are verified according to the procedure in
[RFC8032], Section 5.1.7.
Ed448 signatures are verified according to the procedure in
[RFC8032], Section 5.2.7.
8. SSHFP DNS Resource Records
Usage and generation of the SSHFP DNS resource record is described in
[RFC4255]. The generation of SSHFP resource records for "ssh-
ed25519" keys is described in [RFC7479]. This section illustrates
the generation of SSHFP resource records for "ssh-ed448" keys, and
this document also specifies the corresponding Ed448 code point to
"SSHFP RR Types for public key algorithms" in the "DNS SSHFP Resource
Record Parameters" IANA registry [IANA-SSHFP].
The generation of SSHFP resource records for "ssh-ed448" keys is
described as follows.
The encoding of Ed448 public keys is described in [ED448]. In brief,
an Ed448 public key is a 57-octet value representing a 455-bit
y-coordinate of an elliptic curve point, and a sign bit indicating
the corresponding x-coordinate.
The SSHFP Resource Record for the Ed448 public key with SHA-256
fingerprint would, for example, be:
example.com. IN SSHFP 6 2 ( a87f1b687ac0e57d2a081a2f2826723
34d90ed316d2b818ca9580ea384d924
01 )
The '2' here indicates SHA-256 [RFC6594].
9. IANA Considerations
This document augments the Public Key Algorithm Names in [RFC4250],
Section 4.11.3.
IANA has added the following entry to "Public Key Algorithm Names" in
the "Secure Shell (SSH) Protocol Parameters" registry [IANA-SSH]:
+---------------------------+-----------+
| Public Key Algorithm Name | Reference |
+===========================+===========+
| ssh-ed25519 | RFC 8709 |
+---------------------------+-----------+
| ssh-ed448 | RFC 8709 |
+---------------------------+-----------+
Table 1
IANA has added the following entry to "SSHFP RR Types for public key
algorithms" in the "DNS SSHFP Resource Record Parameters" registry
[IANA-SSHFP]:
+-------+-------------+-----------+
| Value | Description | Reference |
+=======+=============+===========+
| 6 | Ed448 | RFC 8709 |
+-------+-------------+-----------+
Table 2
10. Security Considerations
The security considerations in [RFC4251], Section 9 apply to all SSH
implementations, including those using Ed25519 and Ed448.
The security considerations in [RFC8032], Section 8 and [RFC7479],
Section 3 apply to all uses of Ed25519 and Ed448, including those in
SSH.
11. References
11.1. Normative References
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119,
DOI 10.17487/RFC2119, March 1997,
<https://www.rfc-editor.org/info/rfc2119>.
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Assigned Numbers", RFC 4250,
DOI 10.17487/RFC4250, January 2006,
<https://www.rfc-editor.org/info/rfc4250>.
[RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251,
January 2006, <https://www.rfc-editor.org/info/rfc4251>.
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253,
January 2006, <https://www.rfc-editor.org/info/rfc4253>.
[RFC4255] Schlyter, J. and W. Griffin, "Using DNS to Securely
Publish Secure Shell (SSH) Key Fingerprints", RFC 4255,
DOI 10.17487/RFC4255, January 2006,
<https://www.rfc-editor.org/info/rfc4255>.
[RFC6594] Sury, O., "Use of the SHA-256 Algorithm with RSA, Digital
Signature Algorithm (DSA), and Elliptic Curve DSA (ECDSA)
in SSHFP Resource Records", RFC 6594,
DOI 10.17487/RFC6594, April 2012,
<https://www.rfc-editor.org/info/rfc6594>.
[RFC8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital
Signature Algorithm (EdDSA)", RFC 8032,
DOI 10.17487/RFC8032, January 2017,
<https://www.rfc-editor.org/info/rfc8032>.
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
11.2. Informative References
[ED448] Hamburg, M., "Ed448-Goldilocks, a new elliptic curve",
January 2015, <https://eprint.iacr.org/2015/625.pdf>.
[IANA-SSH] IANA, "Secure Shell (SSH) Protocol Parameters",
<https://www.iana.org/assignments/ssh-parameters>.
[IANA-SSHFP]
IANA, "DNS SSHFP Resource Record Parameters",
<https://www.iana.org/assignments/dns-sshfp-rr-
parameters>.
[OpenSSH-6.5]
Friedl, M., Provos, N., de Raadt, T., Steves, K., Miller,
D., Tucker, D., McIntyre, J., Rice, T., and B. Lindstrom,
"OpenSSH 6.5 release notes", January 2014,
<http://www.openssh.com/txt/release-6.5>.
[RFC7479] Moonesamy, S., "Using Ed25519 in SSHFP Resource Records",
RFC 7479, DOI 10.17487/RFC7479, March 2015,
<https://www.rfc-editor.org/info/rfc7479>.
Acknowledgements
The OpenSSH implementation of Ed25519 in SSH was written by Markus
Friedl. We are also grateful to Mark Baushke, Benjamin Kaduk, and
Daniel Migault for their comments.
Authors' Addresses
Ben Harris
2A Eachard Road
Cambridge
CB3 0HY
United Kingdom
Email: bjh21@bjh21.me.uk
Loganaden Velvindron
cyberstorm.mu
88, Avenue De Plevitz
Roches Brunes
Mauritius
Email: logan@cyberstorm.mu

196
specifications/rfc8758.txt Normal file
View File

@@ -0,0 +1,196 @@
Internet Engineering Task Force (IETF) L. Velvindron
Request for Comments: 8758 cyberstorm.mu
BCP: 227 April 2020
Updates: 4253
Category: Best Current Practice
ISSN: 2070-1721
Deprecating RC4 in Secure Shell (SSH)
Abstract
This document deprecates RC4 in Secure Shell (SSH). Therefore, this
document formally moves RFC 4345 to Historic status.
Status of This Memo
This memo documents an Internet Best Current Practice.
This document is a product of the Internet Engineering Task Force
(IETF). It represents the consensus of the IETF community. It has
received public review and has been approved for publication by the
Internet Engineering Steering Group (IESG). Further information on
BCPs is available in Section 2 of RFC 7841.
Information about the current status of this document, any errata,
and how to provide feedback on it may be obtained at
https://www.rfc-editor.org/info/rfc8758.
Copyright Notice
Copyright (c) 2020 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document. Code Components extracted from this document must
include Simplified BSD License text as described in Section 4.e of
the Trust Legal Provisions and are provided without warranty as
described in the Simplified BSD License.
Table of Contents
1. Introduction
1.1. Requirements Language
2. Updates to RFC 4253
3. IANA Considerations
4. Security Considerations
5. References
5.1. Normative References
5.2. Informative References
Acknowledgements
Author's Address
1. Introduction
The usage of RC4 suites (also designated as "arcfour") for SSH is
specified in [RFC4253] and [RFC4345]. [RFC4253] specifies the
allocation of the "arcfour" cipher for SSH. [RFC4345] specifies and
allocates the "arcfour128" and "arcfour256" ciphers for SSH. RC4
encryption has known weaknesses [RFC7465] [RFC8429]; therefore, this
document starts the deprecation process for their use in Secure Shell
(SSH) [RFC4253]. Accordingly, [RFC4253] is updated to note the
deprecation of the RC4 ciphers, and [RFC4345] is moved to Historic
status, as all ciphers it specifies MUST NOT be used.
1.1. Requirements Language
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
capitals, as shown here.
2. Updates to RFC 4253
[RFC4253] is updated to prohibit arcfour's use in SSH. [RFC4253],
Section 6.3 allocates the "arcfour" cipher by defining a list of
defined ciphers in which the "arcfour" cipher appears as optional, as
shown in Table 1.
+---------+----------+----------------------------------------------+
| arcfour | OPTIONAL | the ARCFOUR stream cipher |
| | | with a 128-bit key |
+---------+----------+----------------------------------------------+
Table 1
This document updates the status of the "arcfour" ciphers in the list
found in [RFC4253], Section 6.3 by moving it from OPTIONAL to MUST
NOT.
+---------+----------+----------------------------------------------+
| arcfour | MUST NOT | the ARCFOUR stream cipher |
| | | with a 128-bit key |
+---------+----------+----------------------------------------------+
Table 2
[RFC4253] defines the "arcfour" ciphers with the following text:
| The "arcfour" cipher is the Arcfour stream cipher with 128-bit
| keys. The Arcfour cipher is believed to be compatible with the
| RC4 cipher [SCHNEIER]. Arcfour (and RC4) has problems with weak
| keys, and should be used with caution.
This document updates [RFC4253], Section 6.3 by replacing the text
above with the following text:
| The "arcfour" cipher is the Arcfour stream cipher with 128-bit
| keys. The Arcfour cipher is compatible with the RC4 cipher
| [SCHNEIER]. Arcfour (and RC4) has known weaknesses [RFC7465]
| [RFC8429] and MUST NOT be used.
3. IANA Considerations
The IANA has updated the "Encryption Algorithm Names" subregistry in
the "Secure Shell (SSH) Protocol Parameters" registry [IANA]. The
registration procedure is IETF review, which is achieved by this
document. The registry has been updated as follows:
+---------------------------+-----------+----------+
| Encryption Algorithm Name | Reference | Note |
+===========================+===========+==========+
| arcfour | RFC 8758 | HISTORIC |
+---------------------------+-----------+----------+
| arcfour128 | RFC 8758 | HISTORIC |
+---------------------------+-----------+----------+
| arcfour256 | RFC 8758 | HISTORIC |
+---------------------------+-----------+----------+
Table 3
4. Security Considerations
This document only prohibits the use of RC4 in SSH; it introduces no
new security considerations.
5. References
5.1. Normative References
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119,
DOI 10.17487/RFC2119, March 1997,
<https://www.rfc-editor.org/info/rfc2119>.
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
5.2. Informative References
[IANA] "Secure Shell (SSH) Protocol Parameters",
<https://www.iana.org/assignments/ssh-parameters>.
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253,
January 2006, <https://www.rfc-editor.org/info/rfc4253>.
[RFC4345] Harris, B., "Improved Arcfour Modes for the Secure Shell
(SSH) Transport Layer Protocol", RFC 4345,
DOI 10.17487/RFC4345, January 2006,
<https://www.rfc-editor.org/info/rfc4345>.
[RFC7465] Popov, A., "Prohibiting RC4 Cipher Suites", RFC 7465,
DOI 10.17487/RFC7465, February 2015,
<https://www.rfc-editor.org/info/rfc7465>.
[RFC8429] Kaduk, B. and M. Short, "Deprecate Triple-DES (3DES) and
RC4 in Kerberos", BCP 218, RFC 8429, DOI 10.17487/RFC8429,
October 2018, <https://www.rfc-editor.org/info/rfc8429>.
[SCHNEIER] Schneier, B., "Applied Cryptography Second Edition:
Protocols, Algorithms, and Source in Code in C", John
Wiley and Sons New York, NY, 1996.
Acknowledgements
The author would like to thank Eric Rescorla, Daniel Migault, and
Rich Salz.
Author's Address
Loganaden Velvindron
cyberstorm.mu
Mauritius
Email: logan@cyberstorm.mu

1028
specifications/rfc9142.txt Normal file

File diff suppressed because it is too large Load Diff

272
specifications/rfc9519.txt Normal file
View File

@@ -0,0 +1,272 @@
Internet Engineering Task Force (IETF) P. Yee
Request for Comments: 9519 AKAYLA
Updates: 4250, 4716, 4819, 8308 January 2024
Category: Standards Track
ISSN: 2070-1721
Update to the IANA SSH Protocol Parameters Registry Requirements
Abstract
This specification updates the registration policies for adding new
entries to registries within the IANA "Secure Shell (SSH) Protocol
Parameters" group of registries. Previously, the registration policy
was generally IETF Review, as defined in RFC 8126, although a few
registries require Standards Action. This specification changes it
from IETF Review to Expert Review. This document updates RFCs 4250,
4716, 4819, and 8308.
Status of This Memo
This is an Internet Standards Track document.
This document is a product of the Internet Engineering Task Force
(IETF). It represents the consensus of the IETF community. It has
received public review and has been approved for publication by the
Internet Engineering Steering Group (IESG). Further information on
Internet Standards is available in Section 2 of RFC 7841.
Information about the current status of this document, any errata,
and how to provide feedback on it may be obtained at
https://www.rfc-editor.org/info/rfc9519.
Copyright Notice
Copyright (c) 2024 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document. Code Components extracted from this document must
include Revised BSD License text as described in Section 4.e of the
Trust Legal Provisions and are provided without warranty as described
in the Revised BSD License.
Table of Contents
1. Introduction
1.1. Requirements Language
2. SSH Protocol Parameters Affected
3. Designated Expert Pool
4. IANA Considerations
5. Security Considerations
6. References
6.1. Normative References
6.2. Informative References
Acknowledgements
Author's Address
1. Introduction
The IANA "Secure Shell (SSH) Protocol Parameters" registry was
populated by several RFCs including [RFC4250], [RFC4716], [RFC4819],
and [RFC8308]. Outside of some narrow value ranges that require
Standards Action in order to add new values or that are marked for
Private Use, the registration policy for other portions of the
registry was IETF Review [RFC8126]. This specification changes the
policy from IETF Review to Expert Review. This change is in line
with similar changes undertaken for certain IPsec and TLS registries.
1.1. Requirements Language
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
capitals, as shown here.
2. SSH Protocol Parameters Affected
The following table lists the "Secure Shell (SSH) Protocol
Parameters" registries whose registration policy has changed from
IETF Review to Expert Review. Where this change applied to a
specific range of values within the particular parameter, that range
is given in the notes column. Affected registries now list this
document as a reference.
+===============================+===========+=======================+
| Parameter Name | RFC | Notes |
+===============================+===========+=======================+
| Authentication Method | [RFC4250] | |
| Names | | |
+-------------------------------+-----------+-----------------------+
| Channel Connection | [RFC4250] | 0x00000001-0xFDFFFFFF |
| Failure Reason Codes | | (inclusive) |
| and Descriptions | | |
+-------------------------------+-----------+-----------------------+
| Compression Algorithm | [RFC4250] | |
| Names | | |
+-------------------------------+-----------+-----------------------+
| Connection Protocol | [RFC4250] | |
| Channel Request Names | | |
+-------------------------------+-----------+-----------------------+
| Connection Protocol | [RFC4250] | |
| Channel Types | | |
+-------------------------------+-----------+-----------------------+
| Connection Protocol | [RFC4250] | |
| Global Request Names | | |
+-------------------------------+-----------+-----------------------+
| Connection Protocol | [RFC4250] | |
| Subsystem Names | | |
+-------------------------------+-----------+-----------------------+
| Disconnection Messages | [RFC4250] | 0x00000001-0xFDFFFFFF |
| Reason Codes and | | (inclusive) |
| Descriptions | | |
+-------------------------------+-----------+-----------------------+
| Encryption Algorithm | [RFC4250] | |
| Names | | |
+-------------------------------+-----------+-----------------------+
| Extended Channel Data | [RFC4250] | 0x00000001-0xFDFFFFFF |
| Transfer data_type_code | | (inclusive) |
| and Data | | |
+-------------------------------+-----------+-----------------------+
| Extension Names | [RFC8308] | |
+-------------------------------+-----------+-----------------------+
| Key Exchange Method | [RFC4250] | |
| Names | | |
+-------------------------------+-----------+-----------------------+
| MAC Algorithm Names | [RFC4250] | |
+-------------------------------+-----------+-----------------------+
| Pseudo-Terminal Encoded | [RFC4250] | |
| Terminal Modes | | |
+-------------------------------+-----------+-----------------------+
| Public Key Algorithm | [RFC4250] | |
| Names | | |
+-------------------------------+-----------+-----------------------+
| Publickey Subsystem | [RFC4819] | |
| Attributes | | |
+-------------------------------+-----------+-----------------------+
| Publickey Subsystem | [RFC4819] | |
| Request Names | | |
+-------------------------------+-----------+-----------------------+
| Publickey Subsystem | [RFC4819] | |
| Response Names | | |
+-------------------------------+-----------+-----------------------+
| Service Names | [RFC4250] | |
+-------------------------------+-----------+-----------------------+
| Signal Names | [RFC4250] | |
+-------------------------------+-----------+-----------------------+
| SSH Public-Key File | [RFC4716] | Excluding header-tags |
| Header Tags | | beginning with x- |
+-------------------------------+-----------+-----------------------+
Table 1: Secure Shell (SSH) Protocol Parameters Affected
The only IANA SSH protocol parameter registries not affected are
"Message Numbers" and "Publickey Subsystem Status Codes", as these
remain Standards Action due to their limited resources as one-byte
registry values.
3. Designated Expert Pool
Expert Review [RFC8126] registry requests are registered after a
three-week review period on the <ssh-reg-review@ietf.org> mailing
list, and on the advice of one or more designated experts. However,
to allow for the allocation of values prior to publication, the
designated experts may approve registration once they are satisfied
that such a specification will be published.
Registration requests sent to the mailing list for review SHOULD use
an appropriate subject (e.g., "Request to register value in SSH
protocol parameters <specific parameter> registry").
Within the review period, the designated experts will either approve
or deny the registration request, communicating this decision to the
review list and IANA. Denials MUST include an explanation and, if
applicable, suggestions as to how to make the request successful.
Registration requests that are undetermined for a period longer than
21 days can be brought to the IESG's attention (using the
<iesg@ietf.org> mailing list) for resolution.
Criteria that SHOULD be applied by the designated experts includes
determining whether the proposed registration duplicates existing
functionality (which is not permitted), whether it is likely to be of
general applicability or useful only for a single application, and
whether the registration description is clear.
IANA MUST only accept registry updates from the designated experts
and the IESG. It SHOULD direct all requests for registration from
other sources to the review mailing list.
It is suggested that multiple designated experts be appointed who are
able to represent the perspectives of different applications using
this specification, in order to enable broadly informed review of
registration decisions. In cases where a registration decision could
be perceived as creating a conflict of interest for a particular
expert, that expert SHOULD defer to the judgment of the other
experts.
4. IANA Considerations
This memo is entirely about updating the IANA "Secure Shell (SSH)
Protocol Parameters" registry.
5. Security Considerations
This memo does not change the Security Considerations for any of the
updated RFCs.
6. References
6.1. Normative References
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119,
DOI 10.17487/RFC2119, March 1997,
<https://www.rfc-editor.org/info/rfc2119>.
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
Protocol Assigned Numbers", RFC 4250,
DOI 10.17487/RFC4250, January 2006,
<https://www.rfc-editor.org/info/rfc4250>.
[RFC4819] Galbraith, J., Van Dyke, J., and J. Bright, "Secure Shell
Public Key Subsystem", RFC 4819, DOI 10.17487/RFC4819,
March 2007, <https://www.rfc-editor.org/info/rfc4819>.
[RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for
Writing an IANA Considerations Section in RFCs", BCP 26,
RFC 8126, DOI 10.17487/RFC8126, June 2017,
<https://www.rfc-editor.org/info/rfc8126>.
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
[RFC8308] Bider, D., "Extension Negotiation in the Secure Shell
(SSH) Protocol", RFC 8308, DOI 10.17487/RFC8308, March
2018, <https://www.rfc-editor.org/info/rfc8308>.
6.2. Informative References
[CURDLE-MA]
Turner, S., "Subject: [Curdle] Time to Review IANA SSH
Registries Policies?", message to the Curdle mailing list,
February 2021,
<https://mailarchive.ietf.org/arch/msg/curdle/
gdiOlZr9bnrZv8umVyguGG3woIM/>.
[RFC4716] Galbraith, J. and R. Thayer, "The Secure Shell (SSH)
Public Key File Format", RFC 4716, DOI 10.17487/RFC4716,
November 2006, <https://www.rfc-editor.org/info/rfc4716>.
Acknowledgements
The impetus for this specification was a February 2021 discussion on
the CURDLE mailing list [CURDLE-MA].
Author's Address
Peter E. Yee
AKAYLA
Mountain View, CA 94043
United States of America
Email: peter@akayla.com

39
src/bin/hush.rs Normal file
View File

@@ -0,0 +1,39 @@
use error_stack::ResultExt;
use hush::config::{BasicClientConfiguration, ClientConfiguration};
#[cfg(not(tarpaulin_include))]
fn main() {
let mut base_config = match BasicClientConfiguration::new(std::env::args()) {
Ok(config) => config,
Err(e) => {
eprintln!("ERROR: {}", e);
return;
}
};
if let Err(e) = base_config.establish_subscribers() {
eprintln!("ERROR: could not set up logging infrastructure: {}", e);
std::process::exit(2);
}
let runtime = match base_config.configured_runtime() {
Ok(runtime) => runtime,
Err(e) => {
tracing::error!(%e, "could not start system runtime");
std::process::exit(3);
}
};
let result = runtime.block_on(async move {
tracing::info!("Starting Hush");
match ClientConfiguration::try_from(base_config).await {
Ok(config) => hush::client::hush(config).await,
Err(e) => Err(e).change_context(hush::OperationalError::ConfigurationError),
}
});
if let Err(e) = result {
tracing::error!("{}", e);
std::process::exit(1);
}
}

4
src/bin/hushd.rs Normal file
View File

@@ -0,0 +1,4 @@
#[cfg(not(tarpaulin_include))]
fn main() {
unimplemented!();
}

164
src/client.rs Normal file
View File

@@ -0,0 +1,164 @@
use crate::config::{ClientCommand, ClientConfiguration};
use crate::crypto::{
CompressionAlgorithm, EncryptionAlgorithm, HostKeyAlgorithm, KeyExchangeAlgorithm, MacAlgorithm,
};
use crate::network::host::Host;
use crate::ssh;
use crate::OperationalError;
use error_stack::{report, Report, ResultExt};
use std::fmt::Display;
use std::str::FromStr;
use thiserror::Error;
pub async fn hush(base_config: ClientConfiguration) -> error_stack::Result<(), OperationalError> {
match base_config.command() {
ClientCommand::ListMacAlgorithms => print_options(MacAlgorithm::allowed()),
ClientCommand::ListHostKeyAlgorithms => print_options(HostKeyAlgorithm::allowed()),
ClientCommand::ListEncryptionAlgorithms => print_options(EncryptionAlgorithm::allowed()),
ClientCommand::ListKeyExchangeAlgorithms => print_options(KeyExchangeAlgorithm::allowed()),
ClientCommand::ListCompressionAlgorithms => print_options(CompressionAlgorithm::allowed()),
ClientCommand::Connect { target } => connect(&base_config, target).await,
}
}
struct Target {
username: String,
host: Host,
port: u16,
}
#[derive(Debug, Error)]
pub enum TargetParseError {
#[error("Invalid port number '{port_string}': {error}")]
InvalidPort {
port_string: String,
error: std::num::ParseIntError,
},
#[error("Invalid hostname '{hostname}': {error}")]
InvalidHostname {
hostname: String,
error: crate::network::host::HostParseError,
},
}
impl FromStr for Target {
type Err = Report<TargetParseError>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (username, host_and_port) = match s.split_once('@') {
None => (whoami::username(), s),
Some((username, rest)) => (username.to_string(), rest),
};
let (port, host) = match host_and_port.rsplit_once(':') {
None => (22, host_and_port),
Some((host, portstr)) => match u16::from_str(portstr) {
Ok(port) => (port, host),
Err(error) => {
return Err(report!(TargetParseError::InvalidPort {
port_string: portstr.to_string(),
error,
})
.attach_printable(format!("from target string {:?}", s)))
}
},
};
let host = Host::from_str(host)
.map_err(|error| {
report!(TargetParseError::InvalidHostname {
hostname: host.to_string(),
error,
})
})
.attach_printable(format!("from target string {:?}", s))?;
Ok(Target {
username,
host,
port,
})
}
}
async fn connect(
base_config: &ClientConfiguration,
target: &str,
) -> error_stack::Result<(), OperationalError> {
let resolver = base_config.resolver();
let target = Target::from_str(target)
.change_context(OperationalError::UnableToParseHostAddress)
.attach_printable_lazy(|| format!("target address '{}'", target))?;
let mut stream = target
.host
.connect(resolver, 22)
.await
.change_context(OperationalError::Connection)?;
let _preamble = ssh::Preamble::read(&mut stream)
.await
.change_context(OperationalError::Connection)?;
// if !commentary.is_empty() {
// tracing::debug!(?commentary, "Server sent commentary.");
// }
// if !pre_message.is_empty() {
// for line in pre_message.lines() {
// tracing::debug!(?line, "Server sent prefix line.");
// }
// }
//
// let my_info = format!(
// "SSH-2.0-{}_{}\r\n",
// env!("CARGO_PKG_NAME"),
// env!("CARGO_PKG_VERSION")
// );
// connection
// .write_all(my_info.as_bytes())
// .await
// .map_err(OperationalError::WriteBanner)?;
//
// assert_eq!(4096, read_buffer.len());
// read_buffer.fill(0);
//
// let mut stream = SshChannel::new(connection);
// let mut rng = rand::thread_rng();
//
// let packet = stream
// .read()
// .await
// .map_err(|error| OperationalError::Read {
// context: "Initial key exchange read",
// error,
// })?
// .expect("has initial packet");
// let message_type = SshMessageID::from(packet.buffer[0]);
// tracing::debug!(size=packet.buffer.len(), %message_type, "Initial buffer received.");
// let keyx = SshKeyExchange::try_from(packet)?;
// println!("{:?}", keyx);
//
// let client_config = config.settings_for("");
// let my_kex = SshKeyExchange::new(&mut rng, client_config)?;
// stream
// .write(my_kex.into())
// .await
// .map_err(|error| OperationalError::Write {
// context: "initial negotiation message",
// error,
// })?;
Ok(())
}
fn print_options<T: Display>(items: &[T]) -> error_stack::Result<(), OperationalError> {
for item in items.iter() {
println!("{}", item);
}
Ok(())
}

298
src/config.rs Normal file
View File

@@ -0,0 +1,298 @@
mod command_line;
mod config_file;
mod console;
mod error;
mod logging;
mod resolver;
use crate::config::console::ConsoleConfiguration;
use crate::config::logging::LoggingConfiguration;
use crate::crypto::known_algorithms::{
ALLOWED_COMPRESSION_ALGORITHMS, ALLOWED_ENCRYPTION_ALGORITHMS, ALLOWED_HOST_KEY_ALGORITHMS,
ALLOWED_KEY_EXCHANGE_ALGORITHMS, ALLOWED_MAC_ALGORITHMS,
};
use crate::crypto::{
CompressionAlgorithm, EncryptionAlgorithm, HostKeyAlgorithm, KeyExchangeAlgorithm, MacAlgorithm,
};
use crate::encodings::ssh::{load_openssh_file_keys, PublicKey};
use clap::Parser;
use config_file::ConfigFile;
use error_stack::ResultExt;
use hickory_resolver::TokioAsyncResolver;
use proptest::arbitrary::Arbitrary;
use proptest::strategy::{BoxedStrategy, Strategy};
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::str::FromStr;
use tokio::runtime::Runtime;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::prelude::*;
pub use self::command_line::ClientCommand;
use self::command_line::CommandLineArguments;
pub use self::error::ConfigurationError;
pub struct BasicClientConfiguration {
runtime: RuntimeConfiguration,
logging: LoggingConfiguration,
config_file: Option<ConfigFile>,
command: ClientCommand,
}
impl BasicClientConfiguration {
/// Load a basic client configuration for this run.
///
/// This will parse the process's command line arguments, and parse
/// a config file if given, but will not interpret this information
/// beyond that required to understand the user's goals for the
/// runtime system and logging. For this reason, it is not async,
/// as it is responsible for determining all the information we
/// will use to generate the runtime.
pub fn new<I, T>(args: I) -> Result<Self, ConfigurationError>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let mut command_line_arguments = CommandLineArguments::try_parse_from(args)?;
let mut config_file = ConfigFile::new(command_line_arguments.config_file.take())?;
let mut runtime = RuntimeConfiguration::default();
let mut logging = LoggingConfiguration::default();
// we prefer the command line to the config file, so first merge
// in the config file so that when we later merge the command line,
// it overwrites any config file options.
if let Some(config_file) = config_file.as_mut() {
config_file.merge_standard_options_into(&mut runtime, &mut logging);
}
command_line_arguments.merge_standard_options_into(&mut runtime, &mut logging);
Ok(BasicClientConfiguration {
runtime,
logging,
config_file,
command: command_line_arguments.command,
})
}
/// Set up the tracing subscribers based on the config file and command line options.
///
/// This will definitely set up our logging substrate, but may also create a subscriber
/// for the console.
#[cfg(not(tarpaulin_include))]
pub fn establish_subscribers(&mut self) -> Result<(), std::io::Error> {
let tracing_layer = self.logging.layer()?;
let mut layers = vec![tracing_layer];
if let Some(console_config) = self.runtime.console.take() {
layers.push(console_config.layer());
}
tracing_subscriber::registry().with(layers).init();
Ok(())
}
/// Generate a new tokio runtime based on the configuration / command line options
/// provided.
#[cfg(not(tarpaulin_include))]
pub fn configured_runtime(&mut self) -> Result<Runtime, std::io::Error> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.max_blocking_threads(self.runtime.tokio_blocking_threads)
.worker_threads(self.runtime.tokio_worker_threads)
.build()
}
}
#[derive(Debug)]
pub struct ClientConnectionOpts {
pub key_exchange_algorithms: Vec<KeyExchangeAlgorithm>,
pub server_host_key_algorithms: Vec<HostKeyAlgorithm>,
pub encryption_algorithms: Vec<EncryptionAlgorithm>,
pub mac_algorithms: Vec<MacAlgorithm>,
pub compression_algorithms: Vec<CompressionAlgorithm>,
pub languages: Vec<String>,
pub predict: Option<KeyExchangeAlgorithm>,
}
impl Default for ClientConnectionOpts {
fn default() -> Self {
ClientConnectionOpts {
key_exchange_algorithms: vec![KeyExchangeAlgorithm::Curve25519Sha256],
server_host_key_algorithms: vec![HostKeyAlgorithm::Ed25519],
encryption_algorithms: vec![EncryptionAlgorithm::Aes256Ctr],
mac_algorithms: vec![MacAlgorithm::HmacSha256],
compression_algorithms: vec![],
languages: vec![],
predict: None,
}
}
}
impl Arbitrary for ClientConnectionOpts {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
let keyx = proptest::sample::select(ALLOWED_KEY_EXCHANGE_ALGORITHMS);
let hostkey = proptest::sample::select(ALLOWED_HOST_KEY_ALGORITHMS);
let enc = proptest::sample::select(ALLOWED_ENCRYPTION_ALGORITHMS);
let mac = proptest::sample::select(ALLOWED_MAC_ALGORITHMS);
let comp = proptest::sample::select(ALLOWED_COMPRESSION_ALGORITHMS);
(
proptest::collection::hash_set(keyx.clone(), 1..ALLOWED_KEY_EXCHANGE_ALGORITHMS.len()),
proptest::collection::hash_set(hostkey, 1..ALLOWED_HOST_KEY_ALGORITHMS.len()),
proptest::collection::hash_set(enc, 1..ALLOWED_ENCRYPTION_ALGORITHMS.len()),
proptest::collection::hash_set(mac, 1..ALLOWED_MAC_ALGORITHMS.len()),
proptest::collection::hash_set(comp, 1..ALLOWED_COMPRESSION_ALGORITHMS.len()),
proptest::option::of(keyx),
)
.prop_map(|(kex, host, enc, mac, comp, pred)| ClientConnectionOpts {
key_exchange_algorithms: finalize_options(kex).unwrap(),
server_host_key_algorithms: finalize_options(host).unwrap(),
encryption_algorithms: finalize_options(enc).unwrap(),
mac_algorithms: finalize_options(mac).unwrap(),
compression_algorithms: finalize_options(comp).unwrap(),
languages: vec![],
predict: pred.and_then(|x| KeyExchangeAlgorithm::from_str(x).ok()),
})
.boxed()
}
}
fn finalize_options<T, E>(inputs: HashSet<&str>) -> Result<Vec<T>, E>
where
T: FromStr<Err = E>,
{
let mut result = vec![];
for item in inputs.into_iter() {
let item = T::from_str(item)?;
result.push(item)
}
Ok(result)
}
#[derive(Default)]
pub struct ServerConfiguration {
runtime: RuntimeConfiguration,
logging: LoggingConfiguration,
}
pub struct RuntimeConfiguration {
tokio_worker_threads: usize,
tokio_blocking_threads: usize,
console: Option<ConsoleConfiguration>,
}
impl Default for RuntimeConfiguration {
fn default() -> Self {
RuntimeConfiguration {
tokio_worker_threads: 4,
tokio_blocking_threads: 16,
console: None,
}
}
}
impl ServerConfiguration {
/// Load a server configuration for this run.
///
/// This will parse the process's command line arguments, and parse
/// a config file if given, so it can take awhile. Even though this
/// function does a bunch of IO, it is not async, because it is expected
/// ro run before we have a tokio runtime fully established. (This
/// function will determine a bunch of things related to the runtime,
/// like how many threads to run, what tracing subscribers to include,
/// etc.)
pub fn new() -> Result<Self, ConfigurationError> {
let mut new_configuration = Self::default();
let mut command_line = CommandLineArguments::parse();
let mut config_file = ConfigFile::new(command_line.config_file.take())?;
// we prefer the command line to the config file, so first merge
// in the config file so that when we later merge the command line,
// it overwrites any config file options.
if let Some(config_file) = config_file.as_mut() {
config_file.merge_standard_options_into(
&mut new_configuration.runtime,
&mut new_configuration.logging,
);
}
command_line.merge_standard_options_into(
&mut new_configuration.runtime,
&mut new_configuration.logging,
);
Ok(new_configuration)
}
}
impl BasicClientConfiguration {}
pub struct ClientConfiguration {
_runtime: RuntimeConfiguration,
_logging: LoggingConfiguration,
resolver: TokioAsyncResolver,
_ssh_keys: HashMap<String, SshKey>,
_defaults: ClientConnectionOpts,
command: ClientCommand,
}
impl ClientConfiguration {
pub async fn try_from(
mut basic: BasicClientConfiguration,
) -> error_stack::Result<Self, ConfigurationError> {
let mut _ssh_keys = HashMap::new();
let mut _defaults = ClientConnectionOpts::default();
let resolver = basic
.config_file
.as_mut()
.and_then(|x| x.resolver.take())
.unwrap_or_default()
.resolver()
.change_context(ConfigurationError::Resolver)?;
let user_ssh_keys = basic
.config_file
.map(|cf| cf.keys)
.unwrap_or_default();
tracing::info!(
provided_ssh_keys = user_ssh_keys.len(),
"loading user-provided SSH keys"
);
for (key_name, key_info) in user_ssh_keys.into_iter() {
let _public_keys = PublicKey::load(key_info.public).await.unwrap();
tracing::info!(?key_name, "public keys loaded");
let _private_key = load_openssh_file_keys(key_info.private, &key_info.password)
.await
.change_context(ConfigurationError::PrivateKey)?;
tracing::info!(?key_name, "private keys loaded");
}
Ok(ClientConfiguration {
_runtime: basic.runtime,
_logging: basic.logging,
resolver,
_ssh_keys,
_defaults,
command: basic.command,
})
}
pub fn command(&self) -> &ClientCommand {
&self.command
}
pub fn resolver(&self) -> &TokioAsyncResolver {
&self.resolver
}
}
pub enum SshKey {
Ed25519 {},
Ecdsa {},
Rsa {},
}

205
src/config/command_line.rs Normal file
View File

@@ -0,0 +1,205 @@
use crate::config::logging::{LogTarget, LoggingConfiguration};
use crate::config::{ConsoleConfiguration, RuntimeConfiguration};
use clap::{Parser, Subcommand};
use std::net::SocketAddr;
use std::path::PathBuf;
#[cfg(test)]
use std::str::FromStr;
use tracing_core::LevelFilter;
#[derive(Parser, Debug, Default)]
#[command(version, about, long_about = None)]
pub struct CommandLineArguments {
/// The config file to use for this command.
#[arg(short, long)]
pub config_file: Option<PathBuf>,
/// The number of "normal" threads to use for this process.
///
/// These are the threads that do the predominant part of the work
/// for the system.
#[arg(short, long)]
threads: Option<usize>,
/// The number of "blocking" threads to use for this process.
///
/// These threads are used for long-running operations, or operations
/// that require extensive interaction with non-asynchronous-friendly
/// IO. This should definitely be >= 1, but does not need to be super
/// high.
#[arg(short, long)]
blocking_threads: Option<usize>,
/// The place to send log data to.
#[arg(short = 'o', long)]
log_file: Option<PathBuf>,
/// The log level to report.
#[arg(short, long)]
log_level: Option<LevelFilter>,
/// A network server IP address to use for tokio-console inspection.
#[arg(short = 's', long, group = "console")]
console_network_server: Option<SocketAddr>,
/// A unix domain socket address to use for tokio-console inspection.
#[arg(short = 'u', long, group = "console")]
console_unix_socket: Option<PathBuf>,
/// The command we're running as the client
#[command(subcommand)]
pub command: ClientCommand,
}
#[derive(Debug, Default, Subcommand)]
pub enum ClientCommand {
/// List the key exchange algorithms we currently allow
#[default]
ListKeyExchangeAlgorithms,
/// List the host key algorithms we currently allow
ListHostKeyAlgorithms,
/// List the encryption algorithms we currently allow
ListEncryptionAlgorithms,
/// List the MAC algorithms we currently allow
ListMacAlgorithms,
/// List the compression algorithms we currently allow
ListCompressionAlgorithms,
/// Connect to the given host and port
Connect { target: String },
}
impl ClientCommand {
/// Is this a command that's just going to list some information to the console?
pub fn is_list_command(&self) -> bool {
matches!(
self,
ClientCommand::ListKeyExchangeAlgorithms
| ClientCommand::ListHostKeyAlgorithms
| ClientCommand::ListEncryptionAlgorithms
| ClientCommand::ListMacAlgorithms
| ClientCommand::ListCompressionAlgorithms
)
}
}
impl CommandLineArguments {
pub fn merge_standard_options_into(
&mut self,
runtime_config: &mut RuntimeConfiguration,
logging_config: &mut LoggingConfiguration,
) {
if let Some(threads) = self.threads {
runtime_config.tokio_worker_threads = threads;
}
if let Some(threads) = self.blocking_threads {
runtime_config.tokio_blocking_threads = threads;
}
if let Some(log_file) = self.log_file.take() {
logging_config.target = LogTarget::File(log_file);
}
if let Some(log_level) = self.log_level.take() {
logging_config.filter = log_level;
} else if self.command.is_list_command() {
logging_config.filter = LevelFilter::ERROR;
}
if (self.console_network_server.is_some() || self.console_unix_socket.is_some())
&& runtime_config.console.is_none()
{
runtime_config.console = Some(ConsoleConfiguration::default());
}
if let Some(cns) = self.console_network_server.take() {
if let Some(x) = runtime_config.console.as_mut() {
x.server_addr = cns.into();
}
}
if let Some(cus) = self.console_unix_socket.take() {
if let Some(x) = runtime_config.console.as_mut() {
x.server_addr = cus.into();
}
}
}
}
#[cfg(test)]
fn apply_command_line(
mut cmdargs: CommandLineArguments,
) -> (RuntimeConfiguration, LoggingConfiguration) {
let mut original_runtime = RuntimeConfiguration::default();
let mut original_logging = LoggingConfiguration::default();
cmdargs.merge_standard_options_into(&mut original_runtime, &mut original_logging);
(original_runtime, original_logging)
}
#[test]
fn command_line_wins() {
let original_runtime = RuntimeConfiguration::default();
let cmd = CommandLineArguments {
threads: Some(original_runtime.tokio_worker_threads + 1),
..CommandLineArguments::default()
};
let (test1_run, _) = apply_command_line(cmd);
assert_ne!(
original_runtime.tokio_worker_threads,
test1_run.tokio_worker_threads
);
assert_eq!(
original_runtime.tokio_blocking_threads,
test1_run.tokio_blocking_threads
);
let cmd = CommandLineArguments {
blocking_threads: Some(original_runtime.tokio_blocking_threads + 1),
..CommandLineArguments::default()
};
let (test2_run, _) = apply_command_line(cmd);
assert_eq!(
original_runtime.tokio_worker_threads,
test2_run.tokio_worker_threads
);
assert_ne!(
original_runtime.tokio_blocking_threads,
test2_run.tokio_blocking_threads
);
}
#[test]
fn can_set_console_settings() {
let cmd = CommandLineArguments {
console_network_server: Some(
SocketAddr::from_str("127.0.0.1:8080").expect("reasonable address"),
),
..CommandLineArguments::default()
};
let (test1_run, _) = apply_command_line(cmd);
assert!(test1_run.console.is_some());
assert!(matches!(
test1_run.console.unwrap().server_addr,
console_subscriber::ServerAddr::Tcp(_)
));
let temp_path = tempfile::NamedTempFile::new()
.expect("can build temp file")
.into_temp_path();
std::fs::remove_file(&temp_path).unwrap();
let filename = temp_path.to_path_buf();
let cmd = CommandLineArguments {
console_unix_socket: Some(filename),
..CommandLineArguments::default()
};
let (test2_run, _) = apply_command_line(cmd);
assert!(test2_run.console.is_some());
assert!(matches!(
test2_run.console.unwrap().server_addr,
console_subscriber::ServerAddr::Unix(_)
));
}

423
src/config/config_file.rs Normal file
View File

@@ -0,0 +1,423 @@
use crate::config::logging::{LogMode, LogTarget, LoggingConfiguration};
use crate::config::resolver::DnsConfig;
use crate::config::RuntimeConfiguration;
use crate::crypto::known_algorithms::{
ALLOWED_COMPRESSION_ALGORITHMS, ALLOWED_ENCRYPTION_ALGORITHMS, ALLOWED_HOST_KEY_ALGORITHMS,
ALLOWED_KEY_EXCHANGE_ALGORITHMS, ALLOWED_MAC_ALGORITHMS,
};
use proptest::arbitrary::{any, Arbitrary};
use proptest::strategy::{BoxedStrategy, Just, Strategy};
use serde::de::{self, Unexpected};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{HashMap, HashSet};
use std::io::Read;
use std::path::PathBuf;
use thiserror::Error;
use tracing_core::Level;
#[allow(dead_code)]
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct ConfigFile {
runtime: Option<RuntimeConfig>,
logging: Option<LoggingConfig>,
pub resolver: Option<DnsConfig>,
pub keys: HashMap<String, KeyConfig>,
defaults: Option<ServerConfig>,
servers: HashMap<String, ServerConfig>,
}
impl Arbitrary for ConfigFile {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
(
any::<Option<RuntimeConfig>>(),
any::<Option<LoggingConfig>>(),
any::<Option<DnsConfig>>(),
keyed_section(KeyConfig::arbitrary()),
any::<Option<ServerConfig>>(),
keyed_section(ServerConfig::arbitrary()),
)
.prop_map(
|(runtime, logging, resolver, keys, defaults, servers)| ConfigFile {
runtime,
logging,
resolver,
keys,
defaults,
servers,
},
)
.boxed()
}
}
fn keyed_section<S>(strat: S) -> BoxedStrategy<HashMap<String, S::Value>>
where
S: Strategy + 'static,
{
proptest::collection::hash_map(
proptest::string::string_regex("[a-zA-Z0-9-]{1,30}").unwrap(),
strat,
0..40,
)
.boxed()
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct RuntimeConfig {
worker_threads: Option<usize>,
blocking_threads: Option<usize>,
}
impl Arbitrary for RuntimeConfig {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
(any::<Option<u16>>(), any::<Option<u16>>())
.prop_map(|(worker_threads, blocking_threads)| RuntimeConfig {
worker_threads: worker_threads.map(Into::into),
blocking_threads: blocking_threads.map(Into::into),
})
.boxed()
}
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct LoggingConfig {
#[serde(
default,
deserialize_with = "parse_level",
serialize_with = "write_level"
)]
level: Option<Level>,
include_filename: Option<bool>,
include_lineno: Option<bool>,
include_thread_ids: Option<bool>,
include_thread_names: Option<bool>,
#[serde(
default,
deserialize_with = "parse_mode",
serialize_with = "write_mode"
)]
mode: Option<LogMode>,
#[serde(
default,
deserialize_with = "parse_target",
serialize_with = "write_target"
)]
target: Option<LogTarget>,
}
impl Arbitrary for LoggingConfig {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
let level_strat = proptest::prop_oneof![
Just(None),
Just(Some(Level::TRACE)),
Just(Some(Level::DEBUG)),
Just(Some(Level::INFO)),
Just(Some(Level::WARN)),
Just(Some(Level::ERROR)),
];
let mode_strat = proptest::prop_oneof![
Just(None),
Just(Some(LogMode::Compact)),
Just(Some(LogMode::Pretty)),
Just(Some(LogMode::Json)),
];
let target_strat = proptest::prop_oneof![
Just(None),
Just(Some(LogTarget::StdErr)),
Just(Some(LogTarget::StdOut)),
Just(Some({
let tempfile = tempfile::NamedTempFile::new().unwrap();
let name = tempfile.into_temp_path();
LogTarget::File(name.to_path_buf())
})),
];
(
level_strat,
any::<Option<bool>>(),
any::<Option<bool>>(),
any::<Option<bool>>(),
any::<Option<bool>>(),
mode_strat,
target_strat,
)
.prop_map(
|(
level,
include_filename,
include_lineno,
include_thread_ids,
include_thread_names,
mode,
target,
)| LoggingConfig {
level,
include_filename,
include_lineno,
include_thread_ids,
include_thread_names,
mode,
target,
},
)
.boxed()
}
}
fn parse_level<'de, D>(deserializer: D) -> Result<Option<Level>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
s.map(|x| match x.to_lowercase().as_str() {
"trace" => Ok(Some(Level::TRACE)),
"debug" => Ok(Some(Level::DEBUG)),
"info" => Ok(Some(Level::INFO)),
"warn" => Ok(Some(Level::WARN)),
"error" => Ok(Some(Level::ERROR)),
_ => Err(de::Error::invalid_value(
Unexpected::Str(&x),
&"valid logging level (trace, debug, info, warn, or error",
)),
})
.unwrap_or_else(|| Ok(None))
}
fn write_level<S: Serializer>(item: &Option<Level>, serializer: S) -> Result<S::Ok, S::Error> {
match item {
None => serializer.serialize_none(),
Some(Level::TRACE) => serializer.serialize_some("trace"),
Some(Level::DEBUG) => serializer.serialize_some("debug"),
Some(Level::INFO) => serializer.serialize_some("info"),
Some(Level::WARN) => serializer.serialize_some("warn"),
Some(Level::ERROR) => serializer.serialize_some("error"),
}
}
fn parse_mode<'de, D>(deserializer: D) -> Result<Option<LogMode>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
s.map(|x| match x.to_lowercase().as_str() {
"compact" => Ok(Some(LogMode::Compact)),
"pretty" => Ok(Some(LogMode::Pretty)),
"json" => Ok(Some(LogMode::Json)),
_ => Err(de::Error::invalid_value(
Unexpected::Str(&x),
&"valid logging level (trace, debug, info, warn, or error",
)),
})
.unwrap_or_else(|| Ok(None))
}
fn write_mode<S: Serializer>(item: &Option<LogMode>, serializer: S) -> Result<S::Ok, S::Error> {
match item {
None => serializer.serialize_none(),
Some(LogMode::Compact) => serializer.serialize_some("compact"),
Some(LogMode::Pretty) => serializer.serialize_some("pretty"),
Some(LogMode::Json) => serializer.serialize_some("json"),
}
}
fn parse_target<'de, D>(deserializer: D) -> Result<Option<LogTarget>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
Ok(s.map(|x| match x.to_lowercase().as_str() {
"stdout" => LogTarget::StdOut,
"stderr" => LogTarget::StdErr,
_ => LogTarget::File(x.into()),
}))
}
fn write_target<S: Serializer>(item: &Option<LogTarget>, serializer: S) -> Result<S::Ok, S::Error> {
match item {
None => serializer.serialize_none(),
Some(LogTarget::StdOut) => serializer.serialize_some("stdout"),
Some(LogTarget::StdErr) => serializer.serialize_some("stderr"),
Some(LogTarget::File(file)) => serializer.serialize_some(file),
}
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct KeyConfig {
pub public: PathBuf,
pub private: PathBuf,
pub password: Option<String>,
}
impl Arbitrary for KeyConfig {
type Parameters = bool;
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_generate_real_keys: Self::Parameters) -> Self::Strategy {
let password = proptest::string::string_regex("[a-zA-Z0-9_!@#$%^&*]{8,40}").unwrap();
proptest::option::of(password)
.prop_map(|password| {
let public_file = tempfile::NamedTempFile::new().unwrap();
let private_file = tempfile::NamedTempFile::new().unwrap();
let public = public_file.into_temp_path().to_path_buf();
let private = private_file.into_temp_path().to_path_buf();
KeyConfig {
public,
private,
password,
}
})
.boxed()
}
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct ServerConfig {
key_exchange_algorithms: Option<Vec<String>>,
server_host_algorithms: Option<Vec<String>>,
encryption_algorithms: Option<Vec<String>>,
mac_algorithms: Option<Vec<String>>,
compression_algorithms: Option<Vec<String>>,
predict: Option<String>,
}
impl Arbitrary for ServerConfig {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
let keyx = proptest::sample::select(ALLOWED_KEY_EXCHANGE_ALGORITHMS);
let hostkey = proptest::sample::select(ALLOWED_HOST_KEY_ALGORITHMS);
let enc = proptest::sample::select(ALLOWED_ENCRYPTION_ALGORITHMS);
let mac = proptest::sample::select(ALLOWED_MAC_ALGORITHMS);
let comp = proptest::sample::select(ALLOWED_COMPRESSION_ALGORITHMS);
(
proptest::collection::hash_set(keyx.clone(), 0..ALLOWED_KEY_EXCHANGE_ALGORITHMS.len()),
proptest::collection::hash_set(hostkey, 0..ALLOWED_HOST_KEY_ALGORITHMS.len()),
proptest::collection::hash_set(enc, 0..ALLOWED_ENCRYPTION_ALGORITHMS.len()),
proptest::collection::hash_set(mac, 0..ALLOWED_MAC_ALGORITHMS.len()),
proptest::collection::hash_set(comp, 0..ALLOWED_COMPRESSION_ALGORITHMS.len()),
proptest::option::of(keyx),
)
.prop_map(|(kex, host, enc, mac, comp, pred)| ServerConfig {
key_exchange_algorithms: finalize_options(kex),
server_host_algorithms: finalize_options(host),
encryption_algorithms: finalize_options(enc),
mac_algorithms: finalize_options(mac),
compression_algorithms: finalize_options(comp),
predict: pred.map(str::to_string),
})
.boxed()
}
}
fn finalize_options(values: HashSet<&str>) -> Option<Vec<String>> {
if values.is_empty() {
None
} else {
Some(values.into_iter().map(str::to_string).collect())
}
}
#[derive(Debug, Error)]
pub enum ConfigFileError {
#[error("Could not open provided config file: {0}")]
CouldNotOpen(std::io::Error),
#[error("Could not read config file: {0}")]
CouldNotRead(std::io::Error),
#[error("Error in config file: {0}")]
ParseError(#[from] toml::de::Error),
}
impl ConfigFile {
/// Try to read in a config file, using the given path if provided, or the XDG-standard
/// path if not.
///
/// If the user didn't provide a config file, and there isn't a config file in the standard
/// XDG place, returns `Ok(None)`.
///
/// This will return errors if there is a parse error in understanding the file, if
/// there's some basic disk error in reading the file, or if the user provided a config
/// file that we couldn't find on disk.
pub fn new(provided_path: Option<PathBuf>) -> Result<Option<Self>, ConfigFileError> {
let config_file = if let Some(path) = provided_path {
let file = std::fs::File::open(path).map_err(ConfigFileError::CouldNotOpen)?;
Some(file)
} else {
let Ok(xdg_base_dirs) = xdg::BaseDirectories::with_prefix("hushd") else {
return Ok(None);
};
let path = xdg_base_dirs.find_config_file("config.toml");
path.and_then(|x| std::fs::File::open(x).ok())
};
let Some(mut config_file) = config_file else {
return Ok(None);
};
let mut contents = String::new();
let _ = config_file
.read_to_string(&mut contents)
.map_err(ConfigFileError::CouldNotRead)?;
let config = toml::from_str(&contents)?;
Ok(Some(config))
}
/// Merge any settings found in the config file into our current configuration.
///
pub fn merge_standard_options_into(
&mut self,
runtime: &mut RuntimeConfiguration,
_logging: &mut LoggingConfiguration,
) {
if let Some(runtime_config) = self.runtime.take() {
runtime.tokio_worker_threads = runtime_config
.worker_threads
.unwrap_or(runtime.tokio_worker_threads);
runtime.tokio_blocking_threads = runtime_config
.blocking_threads
.unwrap_or(runtime.tokio_blocking_threads);
}
}
}
#[test]
fn all_keys_example_parses() {
let path = format!("{}/tests/all_keys.toml", env!("CARGO_MANIFEST_DIR"));
let result = ConfigFile::new(Some(path.into()));
assert!(result.is_ok());
}
proptest::proptest! {
#[test]
fn valid_configs_parse(config in ConfigFile::arbitrary()) {
use std::io::Write;
let mut tempfile = tempfile::NamedTempFile::new().unwrap();
let contents = toml::to_string(&config).unwrap();
tempfile.write_all(contents.as_bytes()).unwrap();
let path = tempfile.into_temp_path();
let parsed = ConfigFile::new(Some(path.to_path_buf())).unwrap().unwrap();
assert_eq!(config, parsed);
}
}

48
src/config/console.rs Normal file
View File

@@ -0,0 +1,48 @@
use console_subscriber::{ConsoleLayer, ServerAddr};
use core::time::Duration;
use std::path::PathBuf;
use tracing_subscriber::Layer;
pub struct ConsoleConfiguration {
client_buffer_capacity: usize,
publish_interval: Duration,
retention: Duration,
pub server_addr: ServerAddr,
poll_duration_histogram_max: Duration,
}
impl Default for ConsoleConfiguration {
fn default() -> Self {
ConsoleConfiguration {
client_buffer_capacity: ConsoleLayer::DEFAULT_CLIENT_BUFFER_CAPACITY,
publish_interval: ConsoleLayer::DEFAULT_PUBLISH_INTERVAL,
retention: ConsoleLayer::DEFAULT_RETENTION,
server_addr: xdg::BaseDirectories::with_prefix("hushd")
.and_then(|x| x.get_runtime_directory().cloned())
.map(|mut v| {
v.push("console.sock");
v
})
.map(|p| ServerAddr::Unix(p.clone()))
.unwrap_or_else(|_| PathBuf::from("console.sock").into()),
poll_duration_histogram_max: ConsoleLayer::DEFAULT_SCHEDULED_DURATION_MAX,
}
}
}
impl ConsoleConfiguration {
#[cfg(not(tarpaulin_include))]
pub fn layer<S>(&self) -> Box<dyn Layer<S> + Send + Sync + 'static>
where
S: tracing_core::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
ConsoleLayer::builder()
.client_buffer_capacity(self.client_buffer_capacity)
.publish_interval(self.publish_interval)
.retention(self.retention)
.server_addr(self.server_addr.clone())
.poll_duration_histogram_max(self.poll_duration_histogram_max)
.spawn()
.boxed()
}
}

22
src/config/error.rs Normal file
View File

@@ -0,0 +1,22 @@
use crate::config::config_file::ConfigFileError;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigurationError {
#[error(transparent)]
ConfigFile(#[from] ConfigFileError),
#[error(transparent)]
CommandLineError(#[from] clap::error::Error),
#[error("Could not read file {file}: {error}")]
CouldNotRead {
file: PathBuf,
error: std::io::Error,
},
#[error("Error loading public key information")]
PublicKey,
#[error("Error loading private key")]
PrivateKey,
#[error("Error configuring DNS resolver")]
Resolver,
}

95
src/config/logging.rs Normal file
View File

@@ -0,0 +1,95 @@
use std::path::PathBuf;
use tracing_core::Subscriber;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::{EnvFilter, Layer};
pub struct LoggingConfiguration {
pub(crate) filter: LevelFilter,
pub(crate) include_filename: bool,
pub(crate) include_lineno: bool,
pub(crate) include_thread_ids: bool,
pub(crate) include_thread_names: bool,
pub(crate) mode: LogMode,
pub(crate) target: LogTarget,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LogMode {
Compact,
Pretty,
Json,
}
#[derive(Clone, Debug, PartialEq)]
pub enum LogTarget {
StdOut,
StdErr,
File(PathBuf),
}
impl LogTarget {
fn supports_ansi(&self) -> bool {
matches!(self, LogTarget::StdOut | LogTarget::StdErr)
}
}
impl Default for LoggingConfiguration {
fn default() -> Self {
LoggingConfiguration {
filter: LevelFilter::INFO,
include_filename: false,
include_lineno: false,
include_thread_ids: true,
include_thread_names: true,
mode: LogMode::Compact,
target: LogTarget::StdErr,
}
}
}
impl LoggingConfiguration {
#[cfg(not(tarpaulin_include))]
pub fn layer<S>(&self) -> Result<Box<dyn Layer<S> + Send + Sync + 'static>, std::io::Error>
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let filter = EnvFilter::builder()
.with_default_directive(self.filter.into())
.with_env_var("HUSHD_LOG")
.from_env_lossy();
let base = tracing_subscriber::fmt::layer()
.with_file(self.include_filename)
.with_line_number(self.include_lineno)
.with_thread_ids(self.include_thread_ids)
.with_thread_names(self.include_thread_names)
.with_ansi(self.target.supports_ansi());
macro_rules! finalize {
($layer: expr) => {
match self.mode {
LogMode::Compact => Ok($layer.compact().with_filter(filter).boxed()),
LogMode::Json => Ok($layer.json().with_filter(filter).boxed()),
LogMode::Pretty => Ok($layer.pretty().with_filter(filter).boxed()),
}
};
}
match self.target {
LogTarget::StdOut => finalize!(base.with_writer(std::io::stdout)),
LogTarget::StdErr => finalize!(base.with_writer(std::io::stderr)),
LogTarget::File(ref path) => {
let log_file = std::fs::File::create(path)?;
finalize!(base.with_writer(std::sync::Mutex::new(log_file)))
}
}
}
}
#[test]
fn supports_ansi() {
assert!(LogTarget::StdOut.supports_ansi());
assert!(LogTarget::StdErr.supports_ansi());
assert!(!LogTarget::File("/dev/null".into()).supports_ansi());
}

304
src/config/resolver.rs Normal file
View File

@@ -0,0 +1,304 @@
use error_stack::report;
use hickory_proto::error::ProtoError;
use hickory_resolver::config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts};
use hickory_resolver::{Name, TokioAsyncResolver};
use proptest::arbitrary::Arbitrary;
use proptest::strategy::{BoxedStrategy, Just, Strategy};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use thiserror::Error;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct DnsConfig {
built_in: Option<BuiltinDnsOption>,
local_domain: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
search_domains: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
name_servers: Vec<ServerConfig>,
#[serde(default)]
timeout_in_seconds: Option<u16>,
#[serde(default)]
retry_attempts: Option<u16>,
#[serde(default)]
cache_size: Option<u32>,
#[serde(default)]
use_hosts_file: Option<bool>,
#[serde(default)]
max_concurrent_requests_for_query: Option<u16>,
#[serde(default)]
preserve_intermediates: Option<bool>,
#[serde(default)]
shuffle_dns_servers: Option<bool>,
}
impl Default for DnsConfig {
fn default() -> Self {
DnsConfig {
built_in: Some(BuiltinDnsOption::Cloudflare),
local_domain: None,
search_domains: vec![],
name_servers: vec![],
timeout_in_seconds: None,
retry_attempts: None,
cache_size: None,
use_hosts_file: None,
max_concurrent_requests_for_query: None,
preserve_intermediates: None,
shuffle_dns_servers: None,
}
}
}
impl Arbitrary for DnsConfig {
type Parameters = bool;
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(always_use_builtin: Self::Parameters) -> Self::Strategy {
if always_use_builtin {
BuiltinDnsOption::arbitrary()
.prop_map(|x| DnsConfig {
built_in: Some(x),
local_domain: None,
search_domains: vec![],
name_servers: vec![],
timeout_in_seconds: None,
retry_attempts: None,
cache_size: None,
use_hosts_file: None,
max_concurrent_requests_for_query: None,
preserve_intermediates: None,
shuffle_dns_servers: None,
})
.boxed()
} else {
let built_in = proptest::option::of(BuiltinDnsOption::arbitrary());
built_in
.prop_flat_map(|built_in| {
let local_domain = proptest::option::of(domain_name_strat());
let search_domains = proptest::collection::vec(domain_name_strat(), 0..10);
let min_servers = if built_in.is_some() { 0 } else { 1 };
let name_servers =
proptest::collection::vec(ServerConfig::arbitrary(), min_servers..6);
let timeout_in_seconds = proptest::option::of(u16::arbitrary());
let retry_attempts = proptest::option::of(u16::arbitrary());
let cache_size = proptest::option::of(u32::arbitrary());
let use_hosts_file = proptest::option::of(bool::arbitrary());
let max_concurrent_requests_for_query = proptest::option::of(u16::arbitrary());
let preserve_intermediates = proptest::option::of(bool::arbitrary());
let shuffle_dns_servers = proptest::option::of(bool::arbitrary());
(
local_domain,
search_domains,
name_servers,
timeout_in_seconds,
retry_attempts,
cache_size,
use_hosts_file,
max_concurrent_requests_for_query,
preserve_intermediates,
shuffle_dns_servers,
)
.prop_map(
move |(
local_domain,
search_domains,
name_servers,
timeout_in_seconds,
retry_attempts,
cache_size,
use_hosts_file,
max_concurrent_requests_for_query,
preserve_intermediates,
shuffle_dns_servers,
)| DnsConfig {
built_in,
local_domain,
search_domains,
name_servers,
timeout_in_seconds,
retry_attempts,
cache_size,
use_hosts_file,
max_concurrent_requests_for_query,
preserve_intermediates,
shuffle_dns_servers,
},
)
})
.boxed()
}
}
}
fn domain_name_strat() -> BoxedStrategy<String> {
let chunk = proptest::string::string_regex("[a-zA-Z0-9]{2,32}").unwrap();
let sets = proptest::collection::vec(chunk, 2..6);
sets.prop_map(|set| {
let mut output = String::new();
for x in set.into_iter() {
if !output.is_empty() {
output.push('.');
}
output.push_str(&x);
}
output
})
.boxed()
}
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
enum BuiltinDnsOption {
Google,
Cloudflare,
Quad9,
}
impl Arbitrary for BuiltinDnsOption {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
proptest::prop_oneof![
Just(BuiltinDnsOption::Google),
Just(BuiltinDnsOption::Cloudflare),
Just(BuiltinDnsOption::Quad9),
]
.boxed()
}
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct ServerConfig {
address: SocketAddr,
#[serde(default)]
trust_negatives: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
bind_address: Option<SocketAddr>,
}
impl Arbitrary for ServerConfig {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
(
SocketAddr::arbitrary(),
bool::arbitrary(),
proptest::option::of(SocketAddr::arbitrary()),
)
.prop_map(|(mut address, trust_negatives, mut bind_address)| {
clear_flow_and_scope_info(&mut address);
if let Some(bind_address) = bind_address.as_mut() {
clear_flow_and_scope_info(bind_address);
}
ServerConfig {
address,
trust_negatives,
bind_address,
}
})
.boxed()
}
}
fn clear_flow_and_scope_info(address: &mut SocketAddr) {
if let SocketAddr::V6(addr) = address {
addr.set_flowinfo(0);
addr.set_scope_id(0);
}
}
#[derive(Debug, Error)]
pub enum ResolverConfigError {
#[error("Bad local domain name '{name}' provided: {error}")]
BadDomainName { name: String, error: ProtoError },
#[error("Bad domain name for search '{name}' provided: {error}")]
BadSearchName { name: String, error: ProtoError },
#[error("No DNS hosts found to search")]
NoHosts,
}
impl DnsConfig {
/// Convert this resolver configuration into an actual ResolverConfig, or say
/// why it's bad.
pub fn resolver(&self) -> error_stack::Result<TokioAsyncResolver, ResolverConfigError> {
let mut config = match &self.built_in {
None => ResolverConfig::new(),
Some(BuiltinDnsOption::Cloudflare) => ResolverConfig::cloudflare(),
Some(BuiltinDnsOption::Google) => ResolverConfig::google(),
Some(BuiltinDnsOption::Quad9) => ResolverConfig::quad9(),
};
if let Some(local_domain) = &self.local_domain {
let name = Name::from_utf8(local_domain).map_err(|error| {
report!(ResolverConfigError::BadDomainName {
name: local_domain.clone(),
error,
})
})?;
config.set_domain(name);
}
for name in self.search_domains.iter() {
let name = Name::from_utf8(name).map_err(|error| {
report!(ResolverConfigError::BadSearchName {
name: name.clone(),
error,
})
})?;
config.add_search(name);
}
for ns in self.name_servers.iter() {
let mut nsconfig = NameServerConfig::new(ns.address, Protocol::Udp);
nsconfig.trust_negative_responses = ns.trust_negatives;
nsconfig.bind_addr = ns.bind_address;
config.add_name_server(nsconfig);
}
if config.name_servers().is_empty() {
return Err(report!(ResolverConfigError::NoHosts));
}
let mut options = ResolverOpts::default();
if let Some(seconds) = self.timeout_in_seconds {
options.timeout = tokio::time::Duration::from_secs(seconds as u64);
}
if let Some(retries) = self.retry_attempts {
options.attempts = retries as usize;
}
if let Some(cache_size) = self.cache_size {
options.cache_size = cache_size as usize;
}
options.use_hosts_file = self.use_hosts_file.unwrap_or(true);
if let Some(max) = self.max_concurrent_requests_for_query {
options.num_concurrent_reqs = max as usize;
}
if let Some(preserve) = self.preserve_intermediates {
options.preserve_intermediates = preserve;
}
if let Some(shuffle) = self.shuffle_dns_servers {
options.shuffle_dns_servers = shuffle;
}
Ok(TokioAsyncResolver::tokio(config, options))
}
}
proptest::proptest! {
#[test]
fn valid_configs_parse(config in DnsConfig::arbitrary_with(false)) {
let toml = toml::to_string(&config).unwrap();
let reversed: DnsConfig = toml::from_str(&toml).unwrap();
assert_eq!(config, reversed);
}
}

272
src/crypto.rs Normal file
View File

@@ -0,0 +1,272 @@
pub mod known_algorithms;
pub mod rsa;
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, PartialEq, Eq)]
pub enum KeyExchangeAlgorithm {
EcdhSha2Nistp256,
EcdhSha2Nistp384,
EcdhSha2Nistp521,
Curve25519Sha256,
}
impl KeyExchangeAlgorithm {
pub fn allowed() -> &'static [KeyExchangeAlgorithm] {
&[
KeyExchangeAlgorithm::EcdhSha2Nistp256,
KeyExchangeAlgorithm::EcdhSha2Nistp384,
KeyExchangeAlgorithm::EcdhSha2Nistp521,
KeyExchangeAlgorithm::Curve25519Sha256,
]
}
}
#[derive(Debug, Error)]
pub enum AlgoFromStrError {
#[error("Did not recognize key exchange algorithm '{0}'")]
UnknownKeyExchangeAlgorithm(String),
#[error("Did not recognize host key algorithm '{0}'")]
UnknownHostKeyAlgorithm(String),
#[error("Did not recognize encryption algorithm '{0}'")]
UnknownEncryptionAlgorithm(String),
#[error("Did not recognize MAC algorithm '{0}'")]
UnknownMacAlgorithm(String),
#[error("Did not recognize compression algorithm '{0}'")]
UnknownCompressionAlgorithm(String),
}
impl FromStr for KeyExchangeAlgorithm {
type Err = AlgoFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ecdh-sha2-nistp256" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp256),
"ecdh-sha2-nistp384" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp384),
"ecdh-sha2-nistp521" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp521),
"curve25519-sha256" => Ok(KeyExchangeAlgorithm::Curve25519Sha256),
other => Err(AlgoFromStrError::UnknownKeyExchangeAlgorithm(
other.to_string(),
)),
}
}
}
impl fmt::Display for KeyExchangeAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyExchangeAlgorithm::EcdhSha2Nistp256 => write!(f, "ecdh-sha2-nistp256"),
KeyExchangeAlgorithm::EcdhSha2Nistp384 => write!(f, "ecdh-sha2-nistp384"),
KeyExchangeAlgorithm::EcdhSha2Nistp521 => write!(f, "ecdh-sha2-nistp521"),
KeyExchangeAlgorithm::Curve25519Sha256 => write!(f, "curve25519-sha256"),
}
}
}
#[test]
fn can_invert_kex_algos() {
for variant in KeyExchangeAlgorithm::allowed().iter() {
let s = variant.to_string();
let reversed = KeyExchangeAlgorithm::from_str(&s);
assert!(reversed.is_ok());
assert_eq!(variant, &reversed.unwrap());
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum HostKeyAlgorithm {
Ed25519,
EcdsaSha2Nistp256,
EcdsaSha2Nistp384,
EcdsaSha2Nistp521,
Rsa,
}
impl HostKeyAlgorithm {
pub fn allowed() -> &'static [Self] {
&[
HostKeyAlgorithm::Ed25519,
HostKeyAlgorithm::EcdsaSha2Nistp256,
HostKeyAlgorithm::EcdsaSha2Nistp384,
HostKeyAlgorithm::EcdsaSha2Nistp521,
HostKeyAlgorithm::Rsa,
]
}
}
impl fmt::Display for HostKeyAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HostKeyAlgorithm::Ed25519 => write!(f, "ssh-ed25519"),
HostKeyAlgorithm::EcdsaSha2Nistp256 => write!(f, "ecdsa-sha2-nistp256"),
HostKeyAlgorithm::EcdsaSha2Nistp384 => write!(f, "ecdsa-sha2-nistp384"),
HostKeyAlgorithm::EcdsaSha2Nistp521 => write!(f, "ecdsa-sha2-nistp521"),
HostKeyAlgorithm::Rsa => write!(f, "ssh-rsa"),
}
}
}
impl FromStr for HostKeyAlgorithm {
type Err = AlgoFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ssh-ed25519" => Ok(HostKeyAlgorithm::Ed25519),
"ecdsa-sha2-nistp256" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp256),
"ecdsa-sha2-nistp384" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp384),
"ecdsa-sha2-nistp521" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp521),
"ssh-rsa" => Ok(HostKeyAlgorithm::Rsa),
unknown => Err(AlgoFromStrError::UnknownHostKeyAlgorithm(
unknown.to_string(),
)),
}
}
}
#[test]
fn can_invert_host_key_algos() {
for variant in HostKeyAlgorithm::allowed().iter() {
let s = variant.to_string();
let reversed = HostKeyAlgorithm::from_str(&s);
assert!(reversed.is_ok());
assert_eq!(variant, &reversed.unwrap());
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum EncryptionAlgorithm {
Aes256Ctr,
Aes256Gcm,
ChaCha20Poly1305,
}
impl EncryptionAlgorithm {
pub fn allowed() -> &'static [Self] {
&[
EncryptionAlgorithm::Aes256Ctr,
EncryptionAlgorithm::Aes256Gcm,
EncryptionAlgorithm::ChaCha20Poly1305,
]
}
}
impl fmt::Display for EncryptionAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EncryptionAlgorithm::Aes256Ctr => write!(f, "aes256-ctr"),
EncryptionAlgorithm::Aes256Gcm => write!(f, "aes256-gcm@openssh.com"),
EncryptionAlgorithm::ChaCha20Poly1305 => write!(f, "chacha20-poly1305@openssh.com"),
}
}
}
impl FromStr for EncryptionAlgorithm {
type Err = AlgoFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"aes256-ctr" => Ok(EncryptionAlgorithm::Aes256Ctr),
"aes256-gcm@openssh.com" => Ok(EncryptionAlgorithm::Aes256Gcm),
"chacha20-poly1305@openssh.com" => Ok(EncryptionAlgorithm::ChaCha20Poly1305),
_ => Err(AlgoFromStrError::UnknownEncryptionAlgorithm(s.to_string())),
}
}
}
#[test]
fn can_invert_encryption_algos() {
for variant in EncryptionAlgorithm::allowed().iter() {
let s = variant.to_string();
let reversed = EncryptionAlgorithm::from_str(&s);
assert!(reversed.is_ok());
assert_eq!(variant, &reversed.unwrap());
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum MacAlgorithm {
HmacSha256,
HmacSha512,
}
impl MacAlgorithm {
pub fn allowed() -> &'static [Self] {
&[MacAlgorithm::HmacSha256, MacAlgorithm::HmacSha512]
}
}
impl fmt::Display for MacAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MacAlgorithm::HmacSha256 => write!(f, "hmac-sha2-256"),
MacAlgorithm::HmacSha512 => write!(f, "hmac-sha2-512"),
}
}
}
impl FromStr for MacAlgorithm {
type Err = AlgoFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"hmac-sha2-256" => Ok(MacAlgorithm::HmacSha256),
"hmac-sha2-512" => Ok(MacAlgorithm::HmacSha512),
_ => Err(AlgoFromStrError::UnknownMacAlgorithm(s.to_string())),
}
}
}
#[test]
fn can_invert_mac_algos() {
for variant in MacAlgorithm::allowed().iter() {
let s = variant.to_string();
let reversed = MacAlgorithm::from_str(&s);
assert!(reversed.is_ok());
assert_eq!(variant, &reversed.unwrap());
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum CompressionAlgorithm {
None,
Zlib,
}
impl CompressionAlgorithm {
pub fn allowed() -> &'static [Self] {
&[CompressionAlgorithm::None, CompressionAlgorithm::Zlib]
}
}
impl fmt::Display for CompressionAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompressionAlgorithm::None => write!(f, "none"),
CompressionAlgorithm::Zlib => write!(f, "zlib"),
}
}
}
impl FromStr for CompressionAlgorithm {
type Err = AlgoFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"none" => Ok(CompressionAlgorithm::None),
"zlib" => Ok(CompressionAlgorithm::Zlib),
_ => Err(AlgoFromStrError::UnknownCompressionAlgorithm(s.to_string())),
}
}
}
#[test]
fn can_invert_compression_algos() {
for variant in CompressionAlgorithm::allowed().iter() {
let s = variant.to_string();
let reversed = CompressionAlgorithm::from_str(&s);
assert!(reversed.is_ok());
assert_eq!(variant, &reversed.unwrap());
}
}

View File

@@ -0,0 +1,24 @@
pub static ALLOWED_KEY_EXCHANGE_ALGORITHMS: &[&str] = &[
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
"curve25519-sha256",
];
pub static ALLOWED_HOST_KEY_ALGORITHMS: &[&str] = &[
"ssh-ed25519",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
"ssh-rsa",
];
pub static ALLOWED_ENCRYPTION_ALGORITHMS: &[&str] = &[
"aes256-ctr",
"aes256-gcm@openssh.com",
"chacha20-poly1305@openssh.com",
];
pub static ALLOWED_MAC_ALGORITHMS: &[&str] = &["hmac-sha2-256", "hmac-sha2-512"];
pub static ALLOWED_COMPRESSION_ALGORITHMS: &[&str] = &["none", "zlib"];

251
src/crypto/rsa.rs Normal file
View File

@@ -0,0 +1,251 @@
use num_bigint_dig::{BigInt, BigUint, ModInverse};
use num_integer::{sqrt, Integer};
use num_traits::Pow;
use zeroize::{Zeroize, Zeroizing};
/// An RSA public key
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct PublicKey {
/// The public modulus, the product of the primes 'p' and 'q'
n: BigUint,
/// The public exponent.
e: BigUint,
}
/// An RSA private key
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct PrivateKey {
/// The public modulus, the product of the primes 'p' and 'q'
n: BigUint,
/// The public exponent
e: BigUint,
/// The private exponent
d: BigUint,
/// The prime 'p'
p: BigUint,
/// The prime 'q'
q: BigUint,
/// d mod (p-1)
dmodp1: BigUint,
/// d mod (q-1)
dmodq1: BigUint,
/// q^-1 mod P
qinv: BigInt,
}
impl Drop for PrivateKey {
fn drop(&mut self) {
self.d.zeroize();
self.p.zeroize();
self.q.zeroize();
self.dmodp1.zeroize();
self.dmodq1.zeroize();
self.qinv.zeroize();
}
}
impl PublicKey {
/// Generate a public key from the given input values.
///
/// No checking is performed at this point to ensure that these
/// values are sane in any way.
pub fn new(n: BigUint, e: BigUint) -> Self {
PublicKey { n, e }
}
}
#[derive(Debug, thiserror::Error)]
pub enum PrivateKeyLoadError {
#[error("Invalid value for public 'e' value; must be between 2^16 and 2^256, got {0}")]
InvalidEValue(BigUint),
#[error("Could not recover primes 'p' and 'q' from provided private key data")]
CouldNotRecoverPrimes,
#[error("Could not generate modular inverse of 'q' in provided private key data")]
CouldNotGenerateModInv,
#[error("Could not cross-confirm value '{value}' in provided private key data")]
IncoherentValue { value: &'static str },
}
impl PrivateKey {
/// Generate a private key from the associated public key and the private
/// value 'd'.
///
/// This will do some further computations, and should not be called when
/// you absolutely must get an answer back immediately. Or, to put it
/// another way, you really should call this with `block_in_place` or
/// similar in an async context.
pub fn from_d(public: &PublicKey, d: BigUint) -> Result<Self, PrivateKeyLoadError> {
let (p, q) = recover_primes(&public.n, &public.e, &d)?;
let dmodp1 = &d % (&p - 1u64);
let dmodq1 = &d % (&q - 1u64);
let qinv = (&q)
.mod_inverse(&p)
.ok_or(PrivateKeyLoadError::CouldNotGenerateModInv)?;
Ok(PrivateKey {
n: public.n.clone(),
e: public.e.clone(),
d,
p,
q,
dmodp1,
dmodq1,
qinv,
})
}
/// Generate a private key from the associated public key, the private
/// value 'd', and several other useful values.
///
/// This will do some additional computations, and should not be called
/// when you absolutely must get an answer back immediately. Or, to put
/// it another way, you really should call this with `block_in_place` or
/// similar.
///
/// This version of this function performs some safety checking to ensure
/// the provided values are reasonable. To avoid this cost, but at the
/// risk of accepting bad key data, use the `_unchecked` variant.
pub fn from_parts(
public: &PublicKey,
d: BigUint,
qinv: BigInt,
p: BigUint,
q: BigUint,
) -> Result<Self, PrivateKeyLoadError> {
let computed_private = Self::from_d(public, d)?;
if qinv != computed_private.qinv {
return Err(PrivateKeyLoadError::IncoherentValue { value: "qinv" });
}
if p != computed_private.p {
return Err(PrivateKeyLoadError::IncoherentValue { value: "p" });
}
if q != computed_private.q {
return Err(PrivateKeyLoadError::IncoherentValue { value: "q" });
}
Ok(computed_private)
}
/// Generate a private key from the associated public key, the private
/// value 'd', and several other useful values.
///
/// This will do some additional computations, and should not be called
/// when you absolutely must get an answer back immediately. Or, to put
/// it another way, you really should call this with `block_in_place` or
/// similar.
///
/// This version of this function performs no safety checking to ensure
/// the provided values are reasonable.
pub fn from_parts_unchecked(
public: &PublicKey,
d: BigUint,
qinv: BigInt,
p: BigUint,
q: BigUint,
) -> Result<Self, PrivateKeyLoadError> {
let dmodp1 = &d % (&p - 1u64);
let dmodq1 = &d % (&q - 1u64);
Ok(PrivateKey {
n: public.n.clone(),
e: public.e.clone(),
d,
qinv,
p,
q,
dmodp1,
dmodq1,
})
}
}
/// Recover the two primes, `p` and `q`, used to generate the given private
/// key.
///
/// This algorithm is as straightforward an implementation of Appendix C.1
/// of NIST 800-56b, revision 2, as I could make it.
fn recover_primes(
n: &BigUint,
e: &BigUint,
d: &BigUint,
) -> Result<(BigUint, BigUint), PrivateKeyLoadError> {
// Assumptions:
// 1. The modulus n is the product of two prime factors p and q, with p > q.
// 2. Both p and q are less than 2^(nBits/2), where nBits ≥ 2048 is the bit length of n.
let n_bits = n.bits() * 8;
let max_p_or_q = BigUint::from(2u8).pow(n_bits / 2);
// 3. The public exponent e is an odd integer between 2^16 and 2^256.
let two = BigUint::from(2u64);
if e < &two.pow(16u64) || e > &two.pow(256u64) {
return Err(PrivateKeyLoadError::InvalidEValue(e.clone()));
}
// 4. The private exponent d is a positive integer that is less than λ(n) = LCM(p 1, q 1).
// 5. The exponents e and d satisfy de ≡ 1 (mod λ(n)).
// Implementation:
// 1. Let a = (de 1) × GCD(n 1, de 1).
let mut de_minus_1 = Zeroizing::new(d * e);
*de_minus_1 -= 1u64;
let n_minus_one = Zeroizing::new(n - 1u64);
let gcd_of_n1_and_de1 = Zeroizing::new(n_minus_one.gcd(&de_minus_1));
let a = Zeroizing::new(&*de_minus_1 * &*gcd_of_n1_and_de1);
// 2. Let m = a/n and r = a mn, so that a = mn + r and 0 ≤ r < n.
let m = Zeroizing::new(&*a / n);
let mn = Zeroizing::new(&*m * n);
if *mn > *a {
// if mn is greater than 'a', then 'r' is going to be negative, which
// violates our assumptions.
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
}
let r = Zeroizing::new(&*a - (&*m * n));
if &*r >= n {
// this violates the other side condition of 2
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
}
// 3. Let b = ( (n r)/(m + 1) ) + 1; if b is not an integer or b^2 ≤ 4n, then output an
// error indicator, and exit without further processing.
let b = Zeroizing::new(((n - &*r) / (&*m + 1u64)) + 1u64);
let b_squared = Zeroizing::new(&*b * &*b);
// 4n contains no secret information, actually, so no need to add the zeorize trait
let four_n = 4usize * n;
if *b_squared <= four_n {
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
}
// 4. Let ϒ be the positive square root of b2 4n; if ϒ is not an integer, then output
// an error indicator, and exit without further processing.
let b_squared_minus_four_n = Zeroizing::new(&*b_squared - four_n);
let y = Zeroizing::new(sqrt((*b_squared_minus_four_n).clone()));
let cross_check = Zeroizing::new(&*y * &*y);
if cross_check != b_squared_minus_four_n {
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
}
// 5. Let p = (b + ϒ)/2 and let q = (b ϒ)/2.
let mut p = &*b + &*y;
p >>= 1;
let mut q = &*b - &*y;
q >>= 1;
// go back and check some of our assumptions from above:
// 1. The modulus n is the product of two prime factors p and q, with p > q.
if n != &(&p * &q) {
p.zeroize();
q.zeroize();
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
}
if p <= q {
p.zeroize();
q.zeroize();
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
}
// 2. Both p and q are less than 2^(nBits/2), where nBits ≥ 2048 is the bit length of n.
if p >= max_p_or_q || q >= max_p_or_q {
p.zeroize();
q.zeroize();
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
}
// 6. Output (p, q) as the prime factors.
Ok((p, q))
}

1
src/encodings.rs Normal file
View File

@@ -0,0 +1 @@
pub mod ssh;

10
src/encodings/ssh.rs Normal file
View File

@@ -0,0 +1,10 @@
pub mod buffer;
mod private_key;
mod private_key_file;
mod public_key;
mod public_key_file;
pub use self::private_key::PrivateKey;
pub use self::private_key_file::{load_openssh_file_keys, PrivateKeyLoadError};
pub use self::public_key::PublicKey;
pub use self::public_key_file::PublicKeyLoadError;

195
src/encodings/ssh/buffer.rs Normal file
View File

@@ -0,0 +1,195 @@
use bytes::{Buf, Bytes};
use error_stack::report;
use thiserror::Error;
/// A read-only buffer formatted according to the SSH standard.
///
/// Reads in this buffer are destructive, in that they will advance
/// an internal pointer, and thus generally require a mutable
/// reference.
pub struct SshReadBuffer<B> {
buffer: B,
}
impl<B: Buf> From<B> for SshReadBuffer<B> {
fn from(buffer: B) -> Self {
SshReadBuffer { buffer }
}
}
#[derive(Debug, Error, PartialEq)]
pub enum SshReadError {
#[error("Attempt to read byte off the end of the buffer")]
CouldNotReadU8,
#[error("Not enough data left in SSH buffer to read a u32 ({remaining} bytes left)")]
CouldNotReadU32 { remaining: usize },
#[error("Not enough data left to read length from SSH buffer ({remaining} bytes left)")]
CouldNotReadLength { remaining: usize },
#[error("Encountered truncated SSH buffer; needed {target} bytes, but only had {remaining}")]
TruncatedBuffer { target: usize, remaining: usize },
#[error("Invalid string in SSH buffer: {error}")]
StringFormatting { error: std::string::FromUtf8Error },
}
impl<B: Buf> SshReadBuffer<B> {
/// Try to read a single byte from the buffer, advancing the pointer.
pub fn get_u8(&mut self) -> error_stack::Result<u8, SshReadError> {
if self.buffer.has_remaining() {
Ok(self.buffer.get_u8())
} else {
Err(report!(SshReadError::CouldNotReadU8))
}
}
/// Read a u32 from the buffer, advancing the pointer
pub fn get_u32(&mut self) -> error_stack::Result<u32, SshReadError> {
let remaining = self.buffer.remaining();
if remaining < 4 {
return Err(report!(SshReadError::CouldNotReadU32 { remaining }));
}
Ok(self.buffer.get_u32())
}
/// Read the next chunk of bytes out of the read buffer.
pub fn get_bytes(&mut self) -> error_stack::Result<Bytes, SshReadError> {
let remaining = self.buffer.remaining();
if remaining < 4 {
return Err(report!(SshReadError::CouldNotReadLength { remaining }));
}
let length = self.buffer.get_u32() as usize;
if length > (remaining - 4) {
return Err(report!(SshReadError::TruncatedBuffer {
target: length,
remaining: self.buffer.remaining(),
}));
}
Ok(self.buffer.copy_to_bytes(length))
}
/// Read the next string from the read buffer.
pub fn get_string(&mut self) -> error_stack::Result<String, SshReadError> {
let bytes = self.get_bytes()?.to_vec();
let string =
String::from_utf8(bytes).map_err(|error| SshReadError::StringFormatting { error })?;
Ok(string)
}
/// Returns true iff there is still data available for reading in the underlying
/// buffer.
pub fn has_remaining(&self) -> bool {
self.buffer.has_remaining()
}
/// Returns the number of bytes remaining in the buffer
pub fn remaining(&self) -> usize {
self.buffer.remaining()
}
}
#[test]
fn empty_gets_error_properly() {
let mut empty: SshReadBuffer<Bytes> = Bytes::new().into();
assert_eq!(
&SshReadError::CouldNotReadU32 { remaining: 0 },
empty.get_u32().unwrap_err().current_context()
);
assert_eq!(
&SshReadError::CouldNotReadLength { remaining: 0 },
empty.get_bytes().unwrap_err().current_context()
);
}
#[test]
fn short_read_errors_properly() {
let mut short: SshReadBuffer<Bytes> = Bytes::from(vec![0]).into();
assert_eq!(
&SshReadError::CouldNotReadU32 { remaining: 1 },
short.get_u32().unwrap_err().current_context()
);
assert_eq!(
&SshReadError::CouldNotReadLength { remaining: 1 },
short.get_bytes().unwrap_err().current_context()
);
}
#[test]
fn truncated_read_errors_properly() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 5, 2, 3]).into();
assert_eq!(
&SshReadError::TruncatedBuffer {
target: 5,
remaining: 2
},
buffer.get_bytes().unwrap_err().current_context()
);
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 5, 2, 3]).into();
assert_eq!(
&SshReadError::TruncatedBuffer {
target: 5,
remaining: 2
},
buffer.get_string().unwrap_err().current_context()
);
}
#[test]
fn bad_string_errors_properly() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 2, 0xC3, 0x28]).into();
assert!(matches!(
buffer.get_string().unwrap_err().current_context(),
SshReadError::StringFormatting { .. },
));
}
#[test]
fn normal_reads_work() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 2, 2, 3]).into();
assert_eq!(2, buffer.get_u32().unwrap());
assert!(buffer.has_remaining());
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 5, 1, 2, 3, 4, 5]).into();
assert_eq!(
Bytes::from(vec![1, 2, 3, 4, 5]),
buffer.get_bytes().unwrap()
);
assert!(!buffer.has_remaining());
let mut buffer: SshReadBuffer<Bytes> =
Bytes::from(vec![0, 0, 0, 5, 0x48, 0x65, 0x6c, 0x6c, 0x6f]).into();
assert_eq!("Hello".to_string(), buffer.get_bytes().unwrap());
assert!(!buffer.has_remaining());
}
#[test]
fn sequential_reads_work() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![
0, 1, 0, 5, 0, 0, 0, 2, 2, 3, 0, 0, 0, 3, 0x66, 0x6f, 0x6f,
])
.into();
assert_eq!(65541, buffer.get_u32().unwrap());
assert_eq!(Bytes::from(vec![2, 3]), buffer.get_bytes().unwrap());
assert_eq!("foo".to_string(), buffer.get_string().unwrap());
assert!(!buffer.has_remaining());
}
#[test]
fn remaining_works() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![
0, 0, 0, 3, 0x66, 0x6f, 0x6f, 0, 0, 0, 3, 0x62, 0x61, 0x72,
])
.into();
assert_eq!("foo".to_string(), buffer.get_string().unwrap());
assert_eq!(7, buffer.remaining());
assert_eq!("bar".to_string(), buffer.get_string().unwrap());
}

View File

@@ -0,0 +1,844 @@
use crate::crypto::rsa;
use crate::encodings::ssh::buffer::SshReadBuffer;
use crate::encodings::ssh::public_key::PublicKey;
use bytes::Buf;
use elliptic_curve::scalar::ScalarPrimitive;
use elliptic_curve::sec1::FromEncodedPoint;
use error_stack::{report, ResultExt};
use num_bigint_dig::{BigInt, BigUint};
pub enum PrivateKey {
Rsa(rsa::PublicKey, rsa::PrivateKey),
P256(p256::PublicKey, p256::SecretKey),
P384(p384::PublicKey, p384::SecretKey),
P521(p521::PublicKey, p521::SecretKey),
Ed25519(ed25519_dalek::VerifyingKey, ed25519_dalek::SigningKey),
}
impl PrivateKey {
// Get a copy of the public key associated with this private key.
//
// This function does do a clone, so will have a memory impact, if that's
// important to you.
pub fn public(&self) -> PublicKey {
match self {
PrivateKey::Rsa(public, _) => PublicKey::Rsa(public.clone()),
PrivateKey::P256(public, _) => PublicKey::P256(*public),
PrivateKey::P384(public, _) => PublicKey::P384(*public),
PrivateKey::P521(public, _) => PublicKey::P521(*public),
PrivateKey::Ed25519(public, _) => PublicKey::Ed25519(*public),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum PrivateKeyReadError {
#[error("No private key type indicator found in alleged private key bytes.")]
NoKeyType,
#[error("Unknown key type for private key: '{key_type}'")]
UnrecognizedKeyType { key_type: String },
#[error("Invalid RSA key in private key bytes")]
BadRsaKey,
#[error("Could not find RSA private key constant {constant}")]
CouldNotFindRsaConstant { constant: &'static str },
#[error("Could not find curve name for private elliptic curve key")]
CouldNotFindCurveName,
#[error("Encoded curve '{curve}' does not match key type '{key}'")]
MismatchedKeyAndCurve { key: String, curve: String },
#[error("Could not read public point for {curve} curve")]
CouldNotReadPublicPoint { curve: String },
#[error("Could not read private scalar for {curve} curve")]
CouldNotReadPrivateScalar { curve: String },
#[error("Invalid scalar for {curve} curve")]
InvalidScalar { curve: String },
#[error("Bad private scalar for {curve} curve: {error}")]
BadPrivateScalar {
curve: String,
error: elliptic_curve::Error,
},
#[error("INTERNAL ERROR: Got way too far with unknown curve {curve}")]
InternalError { curve: String },
#[error("Could not read ed25519 key's {part} data")]
CouldNotReadEd25519 { part: &'static str },
#[error("Invalid ed25519 {kind} key: {error}")]
InvalidEd25519Key { kind: &'static str, error: String },
#[error("Could not decode public point data in private key for {curve} curve: {error}")]
PointDecodeError { curve: String, error: sec1::Error },
#[error("Bad point for public key in curve {curve}")]
BadPointForPublicKey { curve: String },
}
pub fn read_private_key<B: Buf>(
ssh_buffer: &mut SshReadBuffer<B>,
) -> error_stack::Result<PrivateKey, PrivateKeyReadError> {
let encoded_key_type = ssh_buffer
.get_string()
.change_context(PrivateKeyReadError::NoKeyType)?;
match encoded_key_type.as_str() {
"ssh-rsa" => {
let n = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "n" })?;
let e = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "e" })?;
let d = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "d" })?;
let qinv = ssh_buffer.get_bytes().change_context(
PrivateKeyReadError::CouldNotFindRsaConstant { constant: "q⁻¹" },
)?;
let p = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "p" })?;
let q = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "q" })?;
let n = BigUint::from_bytes_be(&n);
let e = BigUint::from_bytes_be(&e);
let d = BigUint::from_bytes_be(&d);
let qinv = BigInt::from_bytes_be(num_bigint_dig::Sign::Plus, &qinv);
let p = BigUint::from_bytes_be(&p);
let q = BigUint::from_bytes_be(&q);
let public_key = rsa::PublicKey::new(n, e);
let private_key = rsa::PrivateKey::from_parts(&public_key, d, qinv, p, q)
.change_context(PrivateKeyReadError::BadRsaKey)?;
Ok(PrivateKey::Rsa(public_key, private_key))
}
"ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" => {
let curve = ssh_buffer
.get_string()
.change_context(PrivateKeyReadError::CouldNotFindCurveName)?;
let max_scalar_byte_length = match (curve.as_str(), encoded_key_type.as_str()) {
("nistp256", "ecdsa-sha2-nistp256") => 32,
("nistp384", "ecdsa-sha2-nistp384") => 48,
("nistp521", "ecdsa-sha2-nistp521") => 66,
_ => {
return Err(report!(PrivateKeyReadError::MismatchedKeyAndCurve {
key: encoded_key_type,
curve,
}))
}
};
let public_key_bytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PrivateKeyReadError::CouldNotReadPublicPoint {
curve: curve.clone(),
}
})?;
let mut scalar_bytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PrivateKeyReadError::CouldNotReadPrivateScalar {
curve: curve.clone(),
}
})?;
while scalar_bytes.remaining() > max_scalar_byte_length {
let zero = scalar_bytes.get_u8();
if zero != 0 {
return Err(report!(PrivateKeyReadError::InvalidScalar { curve }));
}
}
match curve.as_str() {
"nistp256" => {
let public_point =
p256::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| {
report!(PrivateKeyReadError::PointDecodeError {
curve: curve.clone(),
error
})
})?;
let public = p256::PublicKey::from_encoded_point(&public_point)
.into_option()
.ok_or_else(|| {
report!(PrivateKeyReadError::BadPointForPublicKey {
curve: curve.clone(),
})
})?;
let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| {
report!(PrivateKeyReadError::BadPrivateScalar { curve, error })
})?;
let private = p256::SecretKey::new(scalar);
Ok(PrivateKey::P256(public, private))
}
"nistp384" => {
let public_point =
p384::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| {
report!(PrivateKeyReadError::PointDecodeError {
curve: curve.clone(),
error
})
})?;
let public = p384::PublicKey::from_encoded_point(&public_point)
.into_option()
.ok_or_else(|| {
report!(PrivateKeyReadError::BadPointForPublicKey {
curve: curve.clone(),
})
})?;
let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| {
report!(PrivateKeyReadError::BadPrivateScalar { curve, error })
})?;
let private = p384::SecretKey::new(scalar);
Ok(PrivateKey::P384(public, private))
}
"nistp521" => {
let public_point =
p521::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| {
report!(PrivateKeyReadError::PointDecodeError {
curve: curve.clone(),
error
})
})?;
let public = p521::PublicKey::from_encoded_point(&public_point)
.into_option()
.ok_or_else(|| {
report!(PrivateKeyReadError::BadPointForPublicKey {
curve: curve.clone(),
})
})?;
let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| {
report!(PrivateKeyReadError::BadPrivateScalar { curve, error })
})?;
let private = p521::SecretKey::new(scalar);
Ok(PrivateKey::P521(public, private))
}
_ => Err(report!(PrivateKeyReadError::InternalError { curve })),
}
}
"ssh-ed25519" => {
let public_bytes = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotReadEd25519 { part: "public" })?;
let mut private_bytes = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotReadEd25519 { part: "private" })?;
let public_key =
ed25519_dalek::VerifyingKey::try_from(public_bytes.as_ref()).map_err(|error| {
report!(PrivateKeyReadError::InvalidEd25519Key {
kind: "public",
error: format!("{}", error),
})
})?;
if private_bytes.remaining() != 64 {
return Err(report!(PrivateKeyReadError::InvalidEd25519Key {
kind: "private",
error: format!(
"key should be 64 bytes long, saw {}",
private_bytes.remaining()
),
}));
}
let mut private_key = [0; 64];
private_bytes.copy_to_slice(&mut private_key);
let private_key =
ed25519_dalek::SigningKey::from_keypair_bytes(&private_key).map_err(|error| {
report!(PrivateKeyReadError::InvalidEd25519Key {
kind: "private",
error: format!("final load error: {}", error),
})
})?;
Ok(PrivateKey::Ed25519(public_key, private_key))
}
_ => Err(report!(PrivateKeyReadError::UnrecognizedKeyType {
key_type: encoded_key_type,
})),
}
}
#[cfg(test)]
const RSA_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61, 0x00, 0x00, 0x02, 0x01, 0x00,
0xb7, 0x7e, 0xd2, 0x53, 0xf0, 0x92, 0xac, 0x06, 0x53, 0x07, 0x8f, 0xe9, 0x89, 0xd8, 0x92, 0xd4,
0x08, 0x7e, 0xdd, 0x6b, 0xa4, 0x67, 0xd8, 0xac, 0x4a, 0x3b, 0x8f, 0xbd, 0x2f, 0x3a, 0x19, 0x46,
0x7c, 0xa5, 0x7f, 0xc1, 0x01, 0xee, 0xe3, 0xbf, 0x9e, 0xaf, 0xed, 0xc8, 0xbc, 0x8c, 0x30, 0x70,
0x6f, 0xf1, 0xdd, 0xb9, 0x9b, 0x4c, 0x67, 0x7b, 0x8f, 0x7c, 0xcf, 0x85, 0x6f, 0x28, 0x5f, 0xeb,
0xe3, 0x0b, 0x7f, 0x82, 0xf5, 0xa4, 0x99, 0xc6, 0xae, 0x1c, 0xbd, 0xd6, 0xa9, 0x34, 0xc9, 0x05,
0xfc, 0xdc, 0xe2, 0x84, 0x86, 0x69, 0xc5, 0x6b, 0x0a, 0xf5, 0x17, 0x5f, 0x52, 0xda, 0x4a, 0xdf,
0xd9, 0x4a, 0xe2, 0x14, 0x0c, 0xba, 0x96, 0x04, 0x4e, 0x25, 0x38, 0xd1, 0x66, 0x75, 0xf2, 0x27,
0x68, 0x1f, 0x28, 0xce, 0xa5, 0xa3, 0x22, 0x05, 0xf7, 0x9e, 0x38, 0x70, 0xf7, 0x23, 0x65, 0xfe,
0x4e, 0x77, 0x66, 0x70, 0x16, 0x89, 0xa3, 0xa7, 0x1b, 0xbd, 0x6d, 0x94, 0x85, 0xa1, 0x6b, 0xe8,
0xf1, 0xb9, 0xb6, 0x7f, 0x4f, 0xb4, 0x53, 0xa7, 0xfe, 0x2d, 0x89, 0x6a, 0x6e, 0x6d, 0x63, 0x85,
0xe1, 0x00, 0x83, 0x01, 0xb0, 0x00, 0x8a, 0x30, 0xde, 0xdc, 0x2f, 0x30, 0xbc, 0x89, 0x66, 0x2a,
0x28, 0x59, 0x31, 0xd9, 0x74, 0x9c, 0xf2, 0xf1, 0xd7, 0x53, 0xa9, 0x7b, 0xeb, 0x97, 0xfd, 0x53,
0x13, 0x66, 0x59, 0x9d, 0x61, 0x4a, 0x72, 0xf4, 0xa9, 0x22, 0xc8, 0xac, 0x0e, 0xd8, 0x0e, 0x4f,
0x15, 0x59, 0x9b, 0xaa, 0x96, 0xf9, 0xd5, 0x61, 0xd5, 0x04, 0x4c, 0x09, 0x0d, 0x5a, 0x4e, 0x39,
0xd6, 0xbe, 0x16, 0x8c, 0x36, 0xe1, 0x1d, 0x59, 0x5a, 0xa5, 0x5c, 0x50, 0x6b, 0x6f, 0x6a, 0xed,
0x63, 0x04, 0xbc, 0x42, 0xec, 0xcb, 0xea, 0x34, 0xfc, 0x75, 0xcc, 0xd1, 0xca, 0x45, 0x66, 0xd0,
0xc9, 0x14, 0xae, 0x83, 0xd0, 0x7c, 0x0e, 0x06, 0x1d, 0x4f, 0x15, 0x64, 0x53, 0x56, 0xdb, 0xf2,
0x49, 0x83, 0x03, 0xae, 0xda, 0xa7, 0x29, 0x7c, 0x42, 0xbf, 0x82, 0x07, 0xbc, 0x44, 0x09, 0x15,
0x32, 0x4d, 0xc0, 0xdf, 0x8a, 0x04, 0x89, 0xd9, 0xd8, 0xdb, 0x05, 0xa5, 0x60, 0x21, 0xed, 0xcb,
0x54, 0x74, 0x1e, 0x24, 0x06, 0x4d, 0x69, 0x93, 0x72, 0xe8, 0x59, 0xe1, 0x93, 0x1a, 0x6e, 0x48,
0x16, 0x31, 0x38, 0x10, 0x0e, 0x0b, 0x34, 0xeb, 0x20, 0x86, 0x9c, 0x60, 0x68, 0xaf, 0x30, 0x5e,
0x7f, 0x26, 0x37, 0xce, 0xd9, 0xc1, 0x47, 0xdf, 0x2d, 0xba, 0x50, 0x96, 0xcf, 0xf8, 0xf5, 0xe8,
0x65, 0x26, 0x18, 0x4a, 0x88, 0xe0, 0xd8, 0xab, 0x24, 0xde, 0x3f, 0xa9, 0x64, 0x94, 0xe3, 0xaf,
0x7b, 0x43, 0xaa, 0x72, 0x64, 0x7c, 0xef, 0xdb, 0x30, 0x87, 0x7d, 0x70, 0xd7, 0xbe, 0x0a, 0xca,
0x79, 0xe6, 0xb8, 0x3e, 0x23, 0x37, 0x17, 0x7d, 0x0c, 0x41, 0x3d, 0xd9, 0x92, 0xd6, 0x8c, 0x95,
0x8b, 0x63, 0x0b, 0x63, 0x49, 0x98, 0x0f, 0x1f, 0xc1, 0x95, 0x94, 0x6f, 0x22, 0x0e, 0x47, 0x8f,
0xee, 0x12, 0xb9, 0x8e, 0x28, 0xc2, 0x94, 0xa2, 0xd4, 0x0a, 0x79, 0x69, 0x93, 0x8a, 0x6f, 0xf4,
0xae, 0xd1, 0x85, 0x11, 0xbb, 0x6c, 0xd5, 0x41, 0x00, 0x71, 0x9b, 0x24, 0xe4, 0x6d, 0x0a, 0x05,
0x07, 0x4c, 0x28, 0xa6, 0x88, 0x8c, 0xea, 0x74, 0x19, 0x64, 0x26, 0x5a, 0xc8, 0x28, 0xcc, 0xdf,
0xa8, 0xea, 0xa7, 0xda, 0xec, 0x03, 0xcd, 0xcb, 0xf3, 0xd7, 0x6b, 0xb6, 0x4a, 0xd8, 0x50, 0x44,
0x91, 0xde, 0xb2, 0x76, 0x6e, 0x85, 0x21, 0x4b, 0x2f, 0x65, 0x57, 0x76, 0xd3, 0xd9, 0xfa, 0xd2,
0x98, 0xcb, 0x47, 0xaa, 0x33, 0x69, 0x4e, 0x83, 0x75, 0xfe, 0x8e, 0xac, 0x0a, 0xf6, 0xb6, 0xb7,
0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x54, 0x94, 0xd7, 0xdc, 0xab,
0x5a, 0xe0, 0x82, 0xb5, 0xc9, 0x19, 0x94, 0x1b, 0xdf, 0x41, 0xa7, 0x0d, 0x17, 0x75, 0x77, 0x05,
0xbc, 0x7c, 0x8a, 0xc6, 0x58, 0xf8, 0x23, 0xcb, 0x5e, 0x2b, 0x82, 0x6b, 0x38, 0x5a, 0x50, 0x1c,
0x55, 0x02, 0x94, 0x34, 0x50, 0x81, 0xf9, 0xf2, 0xb7, 0x68, 0x28, 0x9b, 0xe1, 0x50, 0x44, 0x1b,
0x0a, 0xb7, 0xf4, 0xa3, 0xaa, 0x73, 0x79, 0xdd, 0x48, 0x2e, 0x16, 0xec, 0x7c, 0x43, 0x55, 0x99,
0x67, 0x3b, 0x1e, 0xf2, 0xe8, 0xfa, 0xb4, 0xb5, 0x20, 0x48, 0xbd, 0x42, 0xd6, 0x8a, 0x6f, 0x6e,
0x09, 0xd9, 0x5f, 0x43, 0x18, 0xc0, 0xa2, 0x46, 0xed, 0xaa, 0x6f, 0xce, 0x98, 0x8e, 0xe7, 0x91,
0x0a, 0x7c, 0xd6, 0x15, 0x33, 0x61, 0x22, 0x5c, 0xe9, 0x67, 0x2a, 0xb4, 0xfb, 0x0f, 0xf3, 0x59,
0x34, 0x7e, 0x1d, 0x64, 0x0b, 0x81, 0x96, 0xc8, 0xc4, 0x7f, 0x62, 0x1e, 0xc7, 0x38, 0xe7, 0xd7,
0xeb, 0xb0, 0x0c, 0xfa, 0x63, 0x71, 0xdc, 0x71, 0x50, 0x7c, 0x0e, 0x4f, 0x46, 0x3c, 0x92, 0x28,
0xaa, 0x45, 0x99, 0x7d, 0x37, 0x7e, 0x4d, 0x1a, 0x03, 0xc0, 0x49, 0x58, 0xf2, 0xc4, 0x70, 0x85,
0xb1, 0x6a, 0x01, 0xa6, 0xe8, 0xb5, 0xb3, 0xf0, 0x64, 0x21, 0x3c, 0xb3, 0x86, 0x91, 0xcc, 0xdb,
0xcc, 0xf0, 0xcb, 0x7b, 0x66, 0xec, 0x0b, 0xdc, 0x08, 0x1e, 0x54, 0x29, 0xf0, 0x16, 0xc4, 0xcd,
0xb0, 0xe4, 0x96, 0x54, 0x54, 0x5d, 0x4d, 0xba, 0x35, 0xeb, 0x3a, 0x96, 0xeb, 0xcc, 0x2e, 0x71,
0x13, 0x4e, 0x41, 0x9f, 0x50, 0x30, 0xc0, 0x47, 0x70, 0x65, 0xf8, 0x91, 0x3c, 0xe3, 0xe5, 0xd3,
0xf2, 0x26, 0x76, 0x26, 0xab, 0x6c, 0x87, 0x01, 0x4e, 0xc5, 0x6a, 0x11, 0x27, 0x80, 0xa4, 0x14,
0xc4, 0xd5, 0xfb, 0x80, 0x97, 0xc8, 0x46, 0xb7, 0xc7, 0x0f, 0xe1, 0xca, 0x95, 0x2b, 0x9d, 0x0c,
0x3b, 0x56, 0x61, 0xe4, 0x39, 0x37, 0xef, 0xeb, 0x3e, 0xcc, 0x72, 0x0b, 0x52, 0x1d, 0xea, 0x39,
0x8a, 0x59, 0x46, 0x78, 0xb0, 0x98, 0xe2, 0xfe, 0x7f, 0xe3, 0x40, 0x81, 0x66, 0x35, 0x1f, 0x8e,
0x75, 0x2a, 0x2f, 0xb7, 0x0d, 0x37, 0x6a, 0x71, 0x8d, 0xb3, 0xef, 0xe1, 0x5c, 0x5d, 0x20, 0xf4,
0xf6, 0x59, 0x1c, 0x75, 0x83, 0x1a, 0xfa, 0x4f, 0x80, 0x72, 0xb6, 0x50, 0x6f, 0xfc, 0xb3, 0x70,
0xcb, 0x68, 0x8a, 0xb4, 0x06, 0x02, 0x3f, 0x33, 0xa4, 0x0e, 0x05, 0xd9, 0x25, 0xeb, 0x7e, 0x35,
0x24, 0xd2, 0x47, 0x64, 0x07, 0x4e, 0xf5, 0x65, 0x4e, 0x16, 0xcf, 0xaa, 0xfe, 0x4a, 0xbe, 0xc3,
0xb7, 0x7a, 0xd4, 0xaa, 0xf1, 0x24, 0x56, 0x60, 0xc8, 0x24, 0x07, 0x03, 0x01, 0xfd, 0x3d, 0x18,
0xec, 0x09, 0x1c, 0xec, 0x63, 0x74, 0x5b, 0xe7, 0x1b, 0x5c, 0x52, 0x30, 0x09, 0x00, 0xa6, 0xbf,
0x6b, 0x46, 0x26, 0xdf, 0x8c, 0x87, 0xde, 0x48, 0x42, 0x29, 0x48, 0x78, 0x55, 0xfb, 0x51, 0xda,
0xe3, 0x82, 0xfc, 0xfd, 0x21, 0x58, 0xb9, 0x7b, 0x17, 0xd0, 0x0a, 0x6a, 0xeb, 0x5a, 0xce, 0xdc,
0x71, 0x10, 0x03, 0xe4, 0x6b, 0x14, 0x4e, 0xda, 0x4e, 0xad, 0x9d, 0xa7, 0x63, 0x6f, 0x71, 0x23,
0xf6, 0x43, 0x0b, 0x43, 0x31, 0x71, 0xfb, 0x7e, 0x8d, 0x49, 0x0c, 0x1e, 0x37, 0x3d, 0x52, 0xad,
0xdb, 0xb7, 0x3a, 0x53, 0x13, 0xf7, 0x64, 0x4d, 0x3a, 0xf5, 0x6b, 0x45, 0x2d, 0xd3, 0xe0, 0x80,
0x16, 0xd5, 0xf4, 0x88, 0x2e, 0xbd, 0xc2, 0x23, 0x35, 0xe9, 0x73, 0xfa, 0x4c, 0x49, 0x63, 0x69,
0x8c, 0x60, 0x6d, 0x21, 0xdf, 0x9b, 0xff, 0xbf, 0xcc, 0xbc, 0x0f, 0xfa, 0x07, 0xa7, 0x6a, 0xcd,
0x43, 0x5b, 0xd5, 0xa3, 0x75, 0x16, 0xa2, 0x9a, 0x10, 0x70, 0x79, 0x00, 0x00, 0x01, 0x01, 0x00,
0xc8, 0xcd, 0xa4, 0x89, 0xf0, 0x84, 0x21, 0x20, 0x16, 0x54, 0x63, 0xa4, 0x1b, 0xcc, 0x68, 0xb9,
0x4e, 0x46, 0x1a, 0xdc, 0xb1, 0x8a, 0x32, 0x24, 0xae, 0x1c, 0xa7, 0x1c, 0x77, 0xfb, 0xd8, 0x37,
0xa4, 0x5b, 0x3c, 0x98, 0x96, 0xd5, 0x11, 0xe0, 0x45, 0xc7, 0xa1, 0xfb, 0xc3, 0x6d, 0x08, 0xf4,
0x0d, 0xf8, 0x13, 0x63, 0x50, 0xf3, 0x93, 0x71, 0x25, 0x47, 0x99, 0xe5, 0x80, 0x3e, 0x62, 0x43,
0x77, 0x3d, 0x58, 0x49, 0xc8, 0x4d, 0xae, 0xb0, 0x2f, 0x3c, 0x5e, 0x08, 0x97, 0x3a, 0xc7, 0x5f,
0x89, 0x3c, 0x44, 0xf0, 0xaa, 0xe9, 0xeb, 0xf4, 0x9a, 0x2d, 0x5c, 0xd4, 0xa7, 0x26, 0xaa, 0xd5,
0x18, 0xec, 0xd9, 0xc9, 0x0f, 0xde, 0xcd, 0xcc, 0xbd, 0xe4, 0xa3, 0x62, 0xed, 0xc0, 0x89, 0xa9,
0x19, 0xb4, 0x4e, 0xc7, 0x89, 0xf9, 0x2f, 0x2a, 0x39, 0x71, 0xfb, 0x00, 0xf8, 0x54, 0x45, 0x73,
0xfe, 0x77, 0x96, 0x32, 0x5a, 0xee, 0xf5, 0x53, 0xc2, 0x62, 0x13, 0x6d, 0x2d, 0x9d, 0x7e, 0xf6,
0x09, 0xf2, 0xd6, 0xf5, 0xb5, 0x32, 0x67, 0x3c, 0x4d, 0xf7, 0x02, 0x45, 0xf7, 0x61, 0x9b, 0x5a,
0x4e, 0x67, 0x2c, 0x7c, 0xeb, 0x2d, 0xde, 0x34, 0xa8, 0xc7, 0xfe, 0x1c, 0x4d, 0x0f, 0x99, 0x13,
0xe2, 0xef, 0x3d, 0x0b, 0xf3, 0x05, 0x79, 0x9d, 0x79, 0x7c, 0x70, 0xda, 0xfe, 0xb8, 0xea, 0x5d,
0xa0, 0x9d, 0x3c, 0xea, 0xc6, 0xe2, 0xc3, 0x9c, 0x42, 0x67, 0xba, 0x0b, 0x78, 0x68, 0xae, 0x5d,
0x49, 0xd1, 0x61, 0x6f, 0xe9, 0x7f, 0x84, 0x51, 0x38, 0x7d, 0x29, 0xfb, 0x9a, 0x3e, 0x06, 0x9d,
0xc1, 0x48, 0xe8, 0xb3, 0xff, 0xf3, 0x1e, 0x10, 0xec, 0x85, 0x99, 0xb5, 0x8b, 0xdd, 0xa6, 0xd6,
0xce, 0xe3, 0x92, 0x3f, 0x74, 0x50, 0x45, 0xc1, 0x80, 0xc3, 0x3b, 0x3e, 0x87, 0xd8, 0x34, 0xae,
0x00, 0x00, 0x01, 0x01, 0x00, 0xf2, 0x1c, 0x2d, 0x0f, 0xc1, 0x24, 0xd0, 0xd6, 0x88, 0xcb, 0x89,
0xb4, 0x73, 0xb6, 0x31, 0xfc, 0x19, 0x0d, 0x5c, 0x46, 0x7f, 0x9c, 0xbd, 0xd6, 0x51, 0x64, 0xd5,
0xaf, 0xbd, 0x0e, 0x40, 0xdb, 0x25, 0x5a, 0x8d, 0x65, 0xc6, 0xcd, 0x2a, 0x8f, 0x76, 0x8a, 0x24,
0x66, 0xe5, 0x7f, 0xf8, 0x3a, 0xb3, 0xf4, 0x7f, 0xf2, 0x8d, 0x55, 0xdc, 0x12, 0x29, 0xb5, 0x08,
0xa3, 0xae, 0xf2, 0xba, 0x69, 0xf8, 0x70, 0xb3, 0x5f, 0xab, 0x5b, 0x2f, 0x07, 0xf5, 0x88, 0xf4,
0x10, 0x4e, 0xbf, 0x40, 0x88, 0xe3, 0xc3, 0x6a, 0x5d, 0x76, 0xc7, 0xf2, 0xb7, 0xdb, 0xf4, 0xfc,
0x6c, 0xcf, 0x85, 0x88, 0xa8, 0x3b, 0x2b, 0x31, 0xfe, 0xc3, 0xc8, 0x33, 0x46, 0xaf, 0x5c, 0x74,
0x15, 0xf7, 0xdf, 0x30, 0x84, 0xb4, 0x4b, 0x42, 0xad, 0x4a, 0xb2, 0xb6, 0x1d, 0x8c, 0x94, 0x18,
0x10, 0x65, 0x27, 0x90, 0xea, 0x4e, 0x51, 0x6e, 0xe4, 0x7e, 0xaa, 0xb2, 0x04, 0x8a, 0x7b, 0xa0,
0x62, 0xef, 0x96, 0x1a, 0x13, 0x6e, 0x04, 0x0a, 0x76, 0x8d, 0xc7, 0x36, 0xf6, 0xb1, 0xc4, 0x70,
0x05, 0x3a, 0x7e, 0x55, 0xbe, 0xba, 0x6c, 0x7a, 0xa0, 0x53, 0x8f, 0xb2, 0x86, 0x96, 0xa5, 0x38,
0x56, 0x16, 0xd1, 0x9b, 0xf7, 0x3e, 0x51, 0x23, 0x4e, 0x01, 0x31, 0x55, 0x0f, 0x4c, 0x5e, 0x45,
0x3b, 0x41, 0x56, 0xfa, 0x3b, 0x4a, 0x09, 0x38, 0x28, 0xe9, 0x16, 0x68, 0xdb, 0x58, 0x49, 0xc3,
0x57, 0x7f, 0x42, 0x47, 0x76, 0xb9, 0x8d, 0x92, 0xf9, 0x3f, 0xb0, 0xf3, 0x1c, 0xbe, 0x0d, 0xea,
0xcf, 0xf9, 0x97, 0xf6, 0x94, 0xbd, 0x86, 0xed, 0xd2, 0x04, 0x02, 0xbb, 0x8a, 0xa9, 0xdf, 0x37,
0x11, 0x0f, 0x3d, 0x95, 0xa2, 0xe2, 0xa2, 0x17, 0x1f, 0x6e, 0x4a, 0x2f, 0x1e, 0x94, 0xbf, 0xef,
0x0c, 0x56, 0x5d, 0x42, 0x03, 0x00, 0x00, 0x01, 0x01, 0x00, 0xc2, 0x05, 0xc8, 0x82, 0x2b, 0xc2,
0xa3, 0x14, 0x2f, 0xa2, 0x88, 0xe8, 0x01, 0x77, 0xc5, 0x03, 0x51, 0x65, 0xd6, 0xc2, 0x54, 0xf3,
0x88, 0x72, 0x05, 0x65, 0x33, 0xae, 0x84, 0x25, 0xb9, 0xb7, 0x26, 0xae, 0x2e, 0x96, 0x84, 0xf7,
0x6b, 0x73, 0xc3, 0x13, 0x76, 0x72, 0x05, 0x1c, 0x21, 0x06, 0x50, 0xc0, 0xd9, 0x52, 0xfc, 0xd3,
0x0f, 0xd3, 0x0a, 0x68, 0xdc, 0xbd, 0xf9, 0xe4, 0xc9, 0xaa, 0x61, 0x1e, 0xc0, 0x56, 0x00, 0xc0,
0x5d, 0xf7, 0xdf, 0xd3, 0x87, 0x8a, 0x7f, 0xa6, 0xec, 0xd8, 0x03, 0x21, 0x57, 0x74, 0x47, 0x88,
0xb0, 0x4f, 0x4e, 0x98, 0x72, 0xf1, 0xf9, 0xd8, 0x65, 0xa2, 0x61, 0xfa, 0x83, 0x11, 0xe9, 0x77,
0x43, 0xf1, 0xfb, 0x4f, 0x2d, 0x06, 0x9f, 0x8a, 0xec, 0x59, 0xb0, 0xcd, 0x33, 0x88, 0x9c, 0x1f,
0x9c, 0xbc, 0xe3, 0xf4, 0x34, 0x04, 0xf8, 0xdc, 0x5c, 0x26, 0xd3, 0x6e, 0x91, 0xf3, 0x9a, 0x69,
0xb9, 0x22, 0xde, 0x43, 0xf4, 0x6f, 0xcc, 0x41, 0x4e, 0x9d, 0x40, 0xad, 0xfe, 0xd5, 0x3d, 0xbb,
0x6c, 0x22, 0x62, 0x0e, 0xa2, 0x09, 0xc1, 0xb8, 0xd9, 0x50, 0xe8, 0xe4, 0x1e, 0x74, 0x25, 0xf5,
0x9e, 0x62, 0x55, 0xe5, 0x1b, 0xb4, 0x7e, 0x5c, 0x8b, 0x2d, 0x10, 0xa1, 0x8b, 0x12, 0x66, 0x3f,
0xad, 0xb4, 0x84, 0xe4, 0xa4, 0x07, 0x7a, 0x9f, 0x8f, 0x7e, 0x04, 0xec, 0xf2, 0x38, 0x5c, 0x67,
0x08, 0x0c, 0xae, 0x19, 0xea, 0xac, 0xf1, 0x80, 0xbb, 0x19, 0xe1, 0xb8, 0x1a, 0x7f, 0x49, 0x6f,
0x2a, 0x9e, 0x8c, 0x68, 0xb8, 0x15, 0xd7, 0x6c, 0xaa, 0x4f, 0xed, 0x8f, 0x63, 0x39, 0x6b, 0x8d,
0xb1, 0xa4, 0xbd, 0x84, 0x0a, 0xff, 0x34, 0x8f, 0x8c, 0xa5, 0x27, 0xf2, 0xca, 0x11, 0x28, 0x79,
0x0d, 0x10, 0x6e, 0x58, 0x0d, 0xda, 0x05, 0x07, 0x54, 0x3d,
];
#[test]
fn legit_rsa_key_works() {
let bytes = bytes::Bytes::from(RSA_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
if let Err(ref e) = result {
println!("error = {:?}", e);
}
assert!(matches!(result, Ok(PrivateKey::Rsa(_, _))));
}
#[test]
fn successful_read_leaves_excess() {
let mut test_key = RSA_TEST_KEY.to_vec();
test_key.push(0xa1);
test_key.push(0x23);
test_key.push(0x05);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
if let Err(ref e) = result {
println!("error = {:?}", e);
}
assert!(matches!(result, Ok(PrivateKey::Rsa(_, _))));
assert_eq!(buffer.get_u8().unwrap(), 0xa1);
assert_eq!(buffer.get_u8().unwrap(), 0x23);
assert_eq!(buffer.get_u8().unwrap(), 0x05);
assert!(!buffer.has_remaining());
}
#[test]
fn short_rsa_reads_fail_properly() {
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..23]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"n"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..529]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"e"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..535]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"d"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1247]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"q⁻¹"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1550]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"p"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1750]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"q"
)));
}
#[test]
fn bad_rsa_key_is_bad() {
let mut test_key = RSA_TEST_KEY.to_vec();
test_key[20] += 1;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::BadRsaKey)));
}
#[cfg(test)]
const ED25519_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00,
0x00, 0x00, 0x20, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e, 0xda, 0x9b, 0x06, 0x3c,
0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda, 0xff, 0x85, 0x51, 0x15,
0x63, 0x58, 0xd3, 0x00, 0x00, 0x00, 0x40, 0x7e, 0x5b, 0xf2, 0x9c, 0x9c, 0xea, 0xdf, 0x7f, 0x2a,
0xf5, 0xf1, 0x3d, 0x46, 0xb6, 0xd5, 0xbc, 0x67, 0xac, 0xae, 0xb5, 0x17, 0xaa, 0x56, 0x22, 0x24,
0x9b, 0xa7, 0x20, 0x39, 0x40, 0x00, 0xed, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e,
0xda, 0x9b, 0x06, 0x3c, 0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda,
0xff, 0x85, 0x51, 0x15, 0x63, 0x58, 0xd3,
];
#[test]
fn good_ed25519_key_works() {
let bytes = bytes::Bytes::from(ED25519_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Ok(PrivateKey::Ed25519(_, _))));
assert!(!buffer.has_remaining());
}
#[test]
fn short_ed25519_reads_fail_properly() {
let bytes = bytes::Bytes::from(&ED25519_TEST_KEY[0..20]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::CouldNotReadEd25519 { part } if
part == &"public")));
let bytes = bytes::Bytes::from(&ED25519_TEST_KEY[0..60]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::CouldNotReadEd25519 { part } if
part == &"private")));
}
#[test]
fn catch_invalid_ed25519_public() {
let mut test_key = ED25519_TEST_KEY.to_vec();
test_key[19] = 0;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, .. } if
kind == &"public")));
}
#[test]
fn catch_short_ed25519_length() {
let mut test_key = ED25519_TEST_KEY.to_vec();
test_key[54] -= 1;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, error } if
kind == &"private" && error.contains("key should be"))));
}
#[test]
fn catch_invalid_private_key() {
let mut test_key = ED25519_TEST_KEY.to_vec();
test_key[110] -= 1;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, error } if
kind == &"private" && error.contains("final load"))));
}
#[cfg(test)]
const P256_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x41, 0x04, 0xd7, 0x47, 0x00, 0x93, 0x35, 0xc5, 0x88, 0xc1,
0x67, 0xb5, 0x1d, 0x5f, 0xf1, 0x9b, 0x82, 0x1d, 0xe8, 0x37, 0x21, 0xe7, 0x89, 0xe5, 0x7c, 0x14,
0x6a, 0xd7, 0xfe, 0x43, 0x44, 0xe7, 0x67, 0xd8, 0x05, 0x66, 0xe1, 0x96, 0x12, 0x8f, 0xc9, 0x23,
0x1c, 0x8f, 0x25, 0x0e, 0xa7, 0xf1, 0xcd, 0x76, 0x7a, 0xea, 0xb7, 0x87, 0x24, 0x07, 0x1e, 0x72,
0x63, 0x6b, 0x81, 0xde, 0x20, 0x81, 0xe7, 0x82, 0x00, 0x00, 0x00, 0x21, 0x00, 0xd1, 0x3d, 0x96,
0x67, 0x38, 0xdd, 0xa7, 0xe9, 0x8d, 0x87, 0x6d, 0x6b, 0x98, 0x6f, 0x36, 0x8e, 0x87, 0x82, 0x6b,
0x3a, 0x40, 0x2d, 0x99, 0x88, 0xf3, 0x26, 0x76, 0xf7, 0xe1, 0x3f, 0xff, 0x26,
];
#[test]
fn legit_p256_key_works() {
let bytes = bytes::Bytes::from(P256_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Ok(PrivateKey::P256(_, _))));
}
#[test]
fn check_for_mismatched_curves() {
let mut test_key = P256_TEST_KEY.to_vec();
test_key[32] = '3' as u8;
test_key[33] = '8' as u8;
test_key[34] = '4' as u8;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::MismatchedKeyAndCurve { key, curve } if
key == &"ecdsa-sha2-nistp256" && curve == &"nistp384")));
}
#[test]
fn ecc_short_reads_fail_correctly() {
let bytes = bytes::Bytes::from(&P256_TEST_KEY[0..48]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotReadPublicPoint { .. })));
let bytes = bytes::Bytes::from(&P256_TEST_KEY[0..112]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotReadPrivateScalar { .. })));
}
#[test]
fn p256_long_scalar_fails() {
let mut test_key = P256_TEST_KEY.to_vec();
assert_eq!(0x21, test_key[107]);
test_key[107] += 2;
test_key.push(0);
test_key.push(0);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[test]
fn invalid_p256_fails_appropriately() {
let mut test_key = P256_TEST_KEY.to_vec();
assert_eq!(4, test_key[39]);
test_key[39] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
if let Err(ref e) = result {
println!("error: {:?}", e);
}
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::PointDecodeError { .. })));
let mut test_key = P256_TEST_KEY.to_vec();
test_key[64] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::BadPointForPublicKey { .. })));
let mut test_key = P256_TEST_KEY.to_vec();
assert_eq!(0x21, test_key[107]);
test_key[107] = 0x22;
test_key.push(4);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[cfg(test)]
const P384_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x61, 0x04, 0x0d, 0xa3, 0x8b, 0x42, 0x98, 0x15, 0xba, 0x0c,
0x9b, 0xf6, 0x5e, 0xc8, 0x68, 0xc3, 0x1e, 0x44, 0xb2, 0x6f, 0x12, 0x2f, 0xc8, 0x97, 0x81, 0x23,
0x60, 0xa0, 0xc3, 0xaf, 0xf1, 0x3f, 0x5f, 0xd6, 0xea, 0x49, 0x9c, 0xd6, 0x74, 0x34, 0xd0, 0x6a,
0xd0, 0x34, 0xe4, 0xd8, 0x42, 0x00, 0x94, 0x61, 0x63, 0x15, 0x11, 0xb0, 0x63, 0x52, 0xcc, 0xbe,
0xe5, 0xc2, 0x12, 0x33, 0xdc, 0x36, 0x03, 0x60, 0x6c, 0xb9, 0x11, 0xa6, 0xe4, 0x81, 0x64, 0x4a,
0x54, 0x74, 0x2b, 0xfb, 0xbc, 0xff, 0x90, 0xe0, 0x2c, 0x00, 0xc1, 0xae, 0x99, 0x2e, 0x0f, 0xdb,
0x50, 0xec, 0x4c, 0xe8, 0xbd, 0xf1, 0x0f, 0xdc, 0x00, 0x00, 0x00, 0x30, 0x55, 0xc0, 0x13, 0xb0,
0x61, 0x6d, 0xca, 0xf8, 0x09, 0x6f, 0x71, 0x26, 0x16, 0x97, 0x9b, 0x84, 0xe8, 0x37, 0xa9, 0x55,
0xab, 0x73, 0x8a, 0xc3, 0x80, 0xb8, 0xd5, 0x9c, 0x71, 0x21, 0xeb, 0x4b, 0xc5, 0xf6, 0x21, 0xc9,
0x92, 0x0c, 0xa6, 0x43, 0x48, 0x97, 0x18, 0x6c, 0x4f, 0x92, 0x42, 0xba,
];
#[test]
fn legit_p384_key_works() {
let bytes = bytes::Bytes::from(P384_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Ok(PrivateKey::P384(_, _))));
}
#[test]
fn p384_long_scalar_fails() {
let mut test_key = P384_TEST_KEY.to_vec();
assert_eq!(0x30, test_key[139]);
test_key[139] += 2;
test_key.push(0);
test_key.push(0);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[test]
fn invalid_p384_fails_appropriately() {
let mut test_key = P384_TEST_KEY.to_vec();
assert_eq!(4, test_key[39]);
test_key[39] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::PointDecodeError { .. })));
let mut test_key = P384_TEST_KEY.to_vec();
test_key[64] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::BadPointForPublicKey { .. })));
let mut test_key = P384_TEST_KEY.to_vec();
assert_eq!(0x30, test_key[139]);
test_key[139] = 0x31;
test_key.push(4);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[cfg(test)]
const P521_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x85, 0x04, 0x01, 0x68, 0x9a, 0x37, 0xac, 0xa3, 0x16, 0x26,
0xa4, 0xaa, 0x72, 0xe6, 0x24, 0x40, 0x4c, 0x69, 0xbf, 0x11, 0x9e, 0xcd, 0xb6, 0x63, 0x92, 0x10,
0xa6, 0xb7, 0x6e, 0x98, 0xb4, 0xa0, 0x81, 0xc5, 0x3c, 0x88, 0xfa, 0x9b, 0x60, 0x57, 0x4c, 0x0f,
0xba, 0x36, 0x4e, 0xc6, 0xe0, 0x3e, 0xa5, 0x86, 0x3d, 0xd3, 0xd5, 0x86, 0x96, 0xe9, 0x4a, 0x1c,
0x0c, 0xe2, 0x70, 0xff, 0x1f, 0x79, 0x06, 0x5d, 0x52, 0x9a, 0x01, 0x2b, 0x87, 0x8e, 0xc2, 0xe9,
0xe2, 0xb7, 0x01, 0x00, 0xa6, 0x1a, 0xf7, 0x23, 0x47, 0x6a, 0x70, 0x10, 0x09, 0x59, 0xde, 0x0a,
0x20, 0xca, 0x2f, 0xd7, 0x5a, 0x98, 0xbd, 0xc3, 0x5b, 0xf2, 0x7b, 0x14, 0x6e, 0x6b, 0xa5, 0x93,
0x5d, 0x3e, 0x21, 0x5c, 0x49, 0x40, 0xbf, 0x9b, 0xc0, 0x78, 0x4b, 0xb1, 0xe9, 0xc7, 0x02, 0xb1,
0x51, 0x94, 0x1a, 0xcf, 0x88, 0x7b, 0xfe, 0xea, 0xd8, 0x55, 0x89, 0xb3, 0x00, 0x00, 0x00, 0x42,
0x01, 0x2d, 0xde, 0x75, 0x5b, 0x7a, 0x04, 0x7e, 0x24, 0xfc, 0x21, 0x07, 0xec, 0xf1, 0xab, 0xb0,
0x21, 0xd6, 0x22, 0xa7, 0xb9, 0x77, 0x72, 0x34, 0x9c, 0xad, 0x32, 0x6f, 0x4f, 0xcc, 0xeb, 0x42,
0xff, 0x4b, 0x9f, 0x78, 0x21, 0x6d, 0xbd, 0x61, 0xc1, 0xe0, 0x9d, 0xb4, 0xca, 0xc0, 0x22, 0xb1,
0xd1, 0xdf, 0xad, 0xe7, 0xed, 0xc4, 0x90, 0x61, 0xe7, 0x7c, 0xab, 0x4a, 0xa9, 0x85, 0x60, 0xd9,
0xad, 0x92,
];
#[test]
fn legit_p521_key_works() {
let bytes = bytes::Bytes::from(P521_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Ok(PrivateKey::P521(_, _))));
}
#[test]
fn p521_long_scalar_fails() {
let mut test_key = P521_TEST_KEY.to_vec();
assert_eq!(0x42, test_key[175]);
test_key[175] += 2;
test_key.push(0);
test_key.push(0);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[test]
fn invalid_p521_fails_appropriately() {
let mut test_key = P521_TEST_KEY.to_vec();
assert_eq!(4, test_key[39]);
test_key[39] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::PointDecodeError { .. })));
let mut test_key = P521_TEST_KEY.to_vec();
test_key[64] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::BadPointForPublicKey { .. })));
let mut test_key = P521_TEST_KEY.to_vec();
assert_eq!(0x42, test_key[175]);
test_key[175] = 0x43;
test_key.push(4);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[test]
fn dont_parse_unknown_key_types() {
let bytes = bytes::Bytes::from(b"\0\0\0\x07ssh-dsa\0\0\0\0".to_vec());
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::UnrecognizedKeyType { key_type } if
key_type.as_str() == "ssh-dsa")));
}

View File

@@ -0,0 +1,816 @@
use super::{PublicKey, PublicKeyLoadError};
use crate::encodings::ssh::buffer::SshReadBuffer;
use crate::encodings::ssh::private_key::{read_private_key, PrivateKey};
use aes::cipher::{KeyIvInit, StreamCipher};
use base64::engine::{self, Engine};
use bytes::{Buf, Bytes};
use error_stack::{report, ResultExt};
use generic_array::GenericArray;
use std::io;
use std::path::Path;
type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
#[derive(Debug, thiserror::Error)]
pub enum PrivateKeyLoadError {
#[error("Could not read private key from file: {error}")]
CouldNotRead { error: io::Error },
#[error("Private key file is lacking necessary newlines.")]
FileLackingNewlines,
#[error("Could not find OpenSSL private key header")]
NoOpenSSLHeader,
#[error("Could not find OpenSSL private key trailer")]
NoOpenSSLTrailer,
#[error("Base64 decoding error: {error}")]
Base64 { error: base64::DecodeError },
#[error("Could not find OpenSSL magic header")]
NoMagicHeader,
#[error("Could not decode OpenSSL magic header: {error}")]
CouldNotDecodeMagicHeader { error: std::str::Utf8Error },
#[error("Unexpected magic value; expected 'openssh-key-v1', got {value}")]
UnexpectedMagicHeaderValue { value: String },
#[error("Could not determine cipher for private key")]
CouldNotDetermineCipher,
#[error("Could not determine KDF for private key")]
CouldNotDetermineKdf,
#[error("Could not determine KDF options for private key")]
CouldNotDetermineKdfOptions,
#[error("Could not determine encoded public key count")]
CouldNotDeterminePublicKeyCount,
#[error("Could not decode encoded public key")]
CouldNotLoadEncodedPublic,
#[error(transparent)]
PublicKeyError(#[from] PublicKeyLoadError),
#[error("Failed to properly decrypt contents ({checkint1} != {checkint2}")]
DecryptionCheckError { checkint1: u32, checkint2: u32 },
#[error("File may have been truncated; should have at least {reported_length} bytes, saw {remaining_bytes}")]
TruncationError {
reported_length: usize,
remaining_bytes: usize,
},
#[error("Very short file; could not find length of private key space")]
CouldNotFindPrivateBufferLength,
#[error("Padding does not match OpenSSH's requirements")]
PaddingError,
#[error("{amount} bytes of extraneous data found at end of private key buffer")]
ExtraneousData { amount: usize },
#[error("Private key does not match associated public key")]
MismatchedPublic,
#[error("Unknown private key encryption scheme '{scheme}'")]
UnknownEncryptionScheme { scheme: String },
#[error("Could not find salt bytes for key derivation")]
CouldNotFindSaltBytes,
#[error("Could not find number of key derivation rounds")]
CouldNotFindKdfRounds,
#[error("Extraneous info in key derivation block")]
ExtraneousKdfInfo,
#[error("Error running key derivation: {error}")]
KeyDerivationError { error: bcrypt_pbkdf::Error },
#[error("Internal error: hit empty encryption method way too late in decryption path")]
EmptyEncryptionWayTooLate,
#[error("Failed to decrypt encrypted data: {error}")]
StreamCipherError {
error: aes::cipher::StreamCipherError,
},
#[error("Could not get {which} post-decryption check bytes")]
CouldNotGetCheckBytes { which: &'static str },
#[error("Failed to load private key")]
CouldNotLoadEncodedPrivate,
#[error("Failed to load info string for private key")]
CouldNotLoadPrivateInfo,
}
#[derive(Debug, PartialEq)]
enum KeyEncryptionMode {
None,
Aes256Ctr,
}
impl KeyEncryptionMode {
fn key_and_iv_size(&self) -> usize {
self.key_size() + self.iv_size()
}
fn key_size(&self) -> usize {
match self {
KeyEncryptionMode::None => 0,
KeyEncryptionMode::Aes256Ctr => 32,
}
}
fn iv_size(&self) -> usize {
match self {
KeyEncryptionMode::None => 0,
KeyEncryptionMode::Aes256Ctr => 16,
}
}
}
#[derive(Debug)]
enum KeyDerivationMethod {
None,
Bcrypt { salt: Bytes, rounds: u32 },
}
#[derive(Debug)]
struct FileEncryptionData {
encryption_mode: KeyEncryptionMode,
key_derivation_method: KeyDerivationMethod,
}
pub async fn load_openssh_file_keys<P: AsRef<Path>>(
path: P,
provided_password: &Option<String>,
) -> error_stack::Result<Vec<(PrivateKey, String)>, PrivateKeyLoadError> {
let path = path.as_ref();
let binary_data = load_openssh_binary_data(path)
.await
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let mut data_buffer = SshReadBuffer::from(binary_data);
let encryption_info = load_encryption_info(&mut data_buffer)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let key_count = data_buffer
.get_u32()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDeterminePublicKeyCount)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let mut public_keys = vec![];
for _ in 0..key_count {
let encoded_public = data_buffer
.get_bytes()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPublic)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let found_public = PublicKey::try_from(encoded_public)
.change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPublic)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
public_keys.push(found_public);
}
let private_key_data = decrypt_private_blob(data_buffer, encryption_info, provided_password)?;
let mut private_key_buffer = SshReadBuffer::from(private_key_data);
let checkint1 = private_key_buffer
.get_u32()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotGetCheckBytes { which: "first" })
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let checkint2 = private_key_buffer
.get_u32()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotGetCheckBytes { which: "second" })
.attach_printable_lazy(|| format!("in {}", path.display()))?;
if checkint1 != checkint2 {
return Err(report!(PrivateKeyLoadError::DecryptionCheckError {
checkint1,
checkint2,
}));
}
let mut results = vec![];
for public_key in public_keys.into_iter() {
let private = read_private_key(&mut private_key_buffer)
.change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPrivate)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
if private.public() != public_key {
return Err(report!(PrivateKeyLoadError::MismatchedPublic));
}
let private_info = private_key_buffer
.get_string()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadPrivateInfo)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
results.push((private, private_info));
}
let mut should_be = 1;
while let Ok(next) = private_key_buffer.get_u8() {
if next != should_be {
return Err(report!(PrivateKeyLoadError::PaddingError))
.attach_printable_lazy(|| format!("in {}", path.display()));
}
should_be += 1;
}
Ok(results)
}
async fn load_openssh_binary_data(path: &Path) -> error_stack::Result<Bytes, PrivateKeyLoadError> {
let file_data = tokio::fs::read_to_string(path)
.await
.map_err(|error| report!(PrivateKeyLoadError::CouldNotRead { error }))?;
let (openssh_header, everything_else) = file_data
.split_once('\n')
.ok_or_else(|| report!(PrivateKeyLoadError::FileLackingNewlines))?;
let (actual_key_data, openssh_trailer) = everything_else
.trim_end()
.rsplit_once('\n')
.ok_or_else(|| report!(PrivateKeyLoadError::FileLackingNewlines))?;
if openssh_header != "-----BEGIN OPENSSH PRIVATE KEY-----" {
return Err(report!(PrivateKeyLoadError::NoOpenSSLHeader));
}
if openssh_trailer != "-----END OPENSSH PRIVATE KEY-----" {
return Err(report!(PrivateKeyLoadError::NoOpenSSLTrailer));
}
let single_line_data: String = actual_key_data
.chars()
.filter(|x| !x.is_whitespace())
.collect();
let mut key_material = engine::general_purpose::STANDARD
.decode(single_line_data)
.map(Bytes::from)
.map_err(|de| report!(PrivateKeyLoadError::Base64 { error: de }))?;
if key_material.remaining() < 15 {
return Err(report!(PrivateKeyLoadError::NoMagicHeader));
}
let auth_magic = std::str::from_utf8(&key_material[0..15])
.map_err(|e| report!(PrivateKeyLoadError::CouldNotDecodeMagicHeader { error: e }))?;
if auth_magic != "openssh-key-v1\0" {
return Err(report!(PrivateKeyLoadError::UnexpectedMagicHeaderValue {
value: auth_magic.to_string(),
}));
}
key_material.advance(15);
Ok(key_material)
}
fn load_encryption_info<B: Buf>(
buffer: &mut SshReadBuffer<B>,
) -> error_stack::Result<FileEncryptionData, PrivateKeyLoadError> {
let cipher_name = buffer
.get_string()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineCipher)?;
let encryption_mode = match cipher_name.as_str() {
"none" => KeyEncryptionMode::None,
"aes256-ctr" => KeyEncryptionMode::Aes256Ctr,
_ => {
return Err(report!(PrivateKeyLoadError::UnknownEncryptionScheme {
scheme: cipher_name,
}))
}
};
let kdf_name = buffer
.get_string()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdf)?;
let key_derivation_method = match kdf_name.as_str() {
"none" => {
let _ = buffer
.get_bytes()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdfOptions)?;
KeyDerivationMethod::None
}
"bcrypt" => {
let mut blob = buffer
.get_bytes()
.map(SshReadBuffer::from)
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdfOptions)?;
let salt = blob
.get_bytes()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotFindSaltBytes)?;
let rounds = blob
.get_u32()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotFindKdfRounds)?;
if blob.has_remaining() {
return Err(report!(PrivateKeyLoadError::ExtraneousKdfInfo));
}
KeyDerivationMethod::Bcrypt { salt, rounds }
}
_ => {
return Err(report!(PrivateKeyLoadError::UnknownEncryptionScheme {
scheme: kdf_name,
}))
}
};
Ok(FileEncryptionData {
encryption_mode,
key_derivation_method,
})
}
fn decrypt_private_blob<B: Buf>(
mut buffer: SshReadBuffer<B>,
encryption_info: FileEncryptionData,
provided_password: &Option<String>,
) -> error_stack::Result<Bytes, PrivateKeyLoadError> {
let ciphertext = buffer
.get_bytes()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotFindPrivateBufferLength)?;
if buffer.has_remaining() {
return Err(report!(PrivateKeyLoadError::ExtraneousData {
amount: buffer.remaining(),
}));
}
if encryption_info.encryption_mode == KeyEncryptionMode::None {
return Ok(ciphertext);
}
let password = match provided_password {
Some(x) => x.as_str(),
None => unimplemented!(),
};
let mut key_and_iv = vec![0; encryption_info.encryption_mode.key_and_iv_size()];
match encryption_info.key_derivation_method {
KeyDerivationMethod::None => {
for (idx, value) in password.as_bytes().iter().enumerate() {
if idx < key_and_iv.len() {
key_and_iv[idx] = *value;
}
}
}
KeyDerivationMethod::Bcrypt { salt, rounds } => {
bcrypt_pbkdf::bcrypt_pbkdf(password, &salt, rounds, &mut key_and_iv)
.map_err(|error| report!(PrivateKeyLoadError::KeyDerivationError { error }))?;
}
}
let key_size = encryption_info.encryption_mode.key_size();
let key = GenericArray::from_slice(&key_and_iv[0..key_size]);
let iv = GenericArray::from_slice(&key_and_iv[key_size..]);
match encryption_info.encryption_mode {
KeyEncryptionMode::None => Err(report!(PrivateKeyLoadError::EmptyEncryptionWayTooLate)),
KeyEncryptionMode::Aes256Ctr => {
let mut out_buffer = vec![0u8; ciphertext.len()];
let mut cipher = Aes256Ctr::new(key, iv);
cipher
.apply_keystream_b2b(&ciphertext, &mut out_buffer)
.map_err(|error| PrivateKeyLoadError::StreamCipherError { error })?;
Ok(out_buffer.into())
}
}
}
#[test]
fn password_generation_matches_saved() {
let salt = [
0x1f, 0x77, 0xd3, 0x33, 0xcc, 0x9e, 0xd1, 0x45, 0xe3, 0xc1, 0xc5, 0x26, 0xa8, 0x7d, 0xf4,
0x1a,
];
let key_and_iv = [
0xcd, 0xd5, 0x5f, 0x6c, 0x73, 0xa0, 0x5c, 0x46, 0x9d, 0xdd, 0x84, 0xbf, 0xab, 0x3a, 0xa6,
0x6e, 0xd6, 0x18, 0xeb, 0x4e, 0x34, 0x1d, 0x89, 0x38, 0x92, 0x4a, 0x0b, 0x5c, 0xca, 0xba,
0x3e, 0xed, 0x42, 0x2e, 0xd3, 0x1f, 0x0b, 0xb7, 0x22, 0x41, 0xeb, 0x3d, 0x37, 0x91, 0xf7,
0x12, 0x15, 0x1b,
];
let password = "foo";
let rounds = 24;
let mut output = [0; 48];
bcrypt_pbkdf::bcrypt_pbkdf(password, &salt, rounds, &mut output).unwrap();
assert_eq!(key_and_iv, output);
}
#[test]
fn can_regenerate_and_decode_saved_session() {
let salt = [
0x86, 0x4a, 0xa5, 0x81, 0x73, 0xb1, 0x12, 0x37, 0x54, 0x6e, 0x34, 0x22, 0x84, 0x89, 0xba,
0x8c,
];
let key_and_iv = [
0x6f, 0xda, 0x3b, 0x95, 0x1d, 0x85, 0xd7, 0xb0, 0x56, 0x8c, 0xc2, 0x4c, 0xa9, 0xf5, 0x95,
0x4c, 0x9b, 0x39, 0x75, 0x14, 0x29, 0x32, 0xac, 0x2b, 0xd3, 0xf8, 0x63, 0x50, 0xc8, 0xfa,
0xcb, 0xb4, 0xca, 0x9a, 0x53, 0xd1, 0xf1, 0x26, 0x26, 0xd8, 0x1a, 0x44, 0x76, 0x2b, 0x27,
0xd0, 0x43, 0x91,
];
let key_length = 32;
let iv_length = 16;
let rounds = 24;
let mut bcrypt_output = [0; 48];
bcrypt_pbkdf::bcrypt_pbkdf("foo", &salt, rounds, &mut bcrypt_output).unwrap();
assert_eq!(key_and_iv, bcrypt_output);
let key_bytes = &key_and_iv[0..key_length];
let iv_bytes = &key_and_iv[key_length..];
assert_eq!(iv_length, iv_bytes.len());
let plaintext = [
0xfc, 0xc7, 0x14, 0xe5, 0xfc, 0xc7, 0x14, 0xe5, 0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68,
0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00, 0x00, 0x00, 0x20, 0x85, 0x50, 0x4f,
0x2e, 0xd7, 0xab, 0x62, 0xf0, 0xc5, 0xe1, 0xaf, 0x7f, 0x20, 0x6b, 0xb7, 0x3d, 0x92, 0x7d,
0xa4, 0x00, 0xf9, 0xdd, 0x08, 0x38, 0x7b, 0xbf, 0x91, 0x3a, 0xd0, 0xfc, 0x00, 0x6d, 0x00,
0x00, 0x00, 0x40, 0x23, 0xfe, 0xe2, 0xb9, 0xae, 0x83, 0x97, 0xa1, 0x7d, 0x4f, 0x45, 0xb2,
0x61, 0x28, 0xeb, 0x6d, 0xd6, 0x5c, 0x38, 0x04, 0x2c, 0xbc, 0x9d, 0xf5, 0x1b, 0x47, 0x3b,
0x89, 0x20, 0x77, 0x6c, 0x8c, 0x85, 0x50, 0x4f, 0x2e, 0xd7, 0xab, 0x62, 0xf0, 0xc5, 0xe1,
0xaf, 0x7f, 0x20, 0x6b, 0xb7, 0x3d, 0x92, 0x7d, 0xa4, 0x00, 0xf9, 0xdd, 0x08, 0x38, 0x7b,
0xbf, 0x91, 0x3a, 0xd0, 0xfc, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x10, 0x61, 0x64, 0x61, 0x6d,
0x77, 0x69, 0x63, 0x6b, 0x40, 0x65, 0x72, 0x67, 0x61, 0x74, 0x65, 0x73, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
];
let ciphertext = [
0x19, 0x96, 0xad, 0x71, 0x8d, 0x07, 0x9d, 0xf3, 0x8d, 0xe9, 0x63, 0x1c, 0xfe, 0xe4, 0xc2,
0x6a, 0x04, 0x12, 0xc1, 0x81, 0xc2, 0xe0, 0xd9, 0x63, 0xf1, 0xb8, 0xf1, 0x00, 0x6a, 0xb4,
0x35, 0xc3, 0x7e, 0x71, 0xa5, 0x65, 0xab, 0x82, 0x66, 0xd9, 0x3e, 0x68, 0x69, 0xa3, 0x01,
0xe1, 0x67, 0x42, 0x0a, 0x7c, 0xe2, 0x92, 0xab, 0x4f, 0x00, 0xfa, 0xaa, 0x20, 0x88, 0x6b,
0xa7, 0x39, 0x75, 0x0f, 0xab, 0xf5, 0x53, 0x47, 0x07, 0x10, 0xb5, 0xfb, 0xf1, 0x86, 0x9e,
0xbb, 0xe9, 0x22, 0x59, 0xa8, 0xdf, 0xf0, 0xa5, 0x28, 0xa5, 0x27, 0x26, 0x1b, 0x05, 0xb1,
0xae, 0xb4, 0xbf, 0x15, 0xa5, 0xbf, 0x64, 0x8a, 0xb3, 0x9c, 0x11, 0x16, 0xa2, 0x01, 0xa7,
0xfd, 0x2d, 0xfa, 0xc6, 0x01, 0xb2, 0xfd, 0xaa, 0x14, 0x38, 0x12, 0x79, 0xb1, 0x8a, 0x86,
0xa8, 0xdb, 0x84, 0xe9, 0xc8, 0xbb, 0x37, 0x36, 0xe4, 0x7d, 0x89, 0xd2, 0x1b, 0xab, 0x79,
0x68, 0x69, 0xb8, 0xe5, 0x04, 0x7a, 0x00, 0x14, 0x5a, 0xa5, 0x96, 0x0a, 0x1d, 0xe9, 0x5a,
0xfc, 0x80, 0x77, 0x06, 0x4d, 0xb9, 0x02, 0x95, 0x2c, 0x34,
];
let key = GenericArray::from_slice(&key_bytes);
let iv = GenericArray::from_slice(&iv_bytes);
let mut conversion_buffer = [0; 160];
let mut cipher = Aes256Ctr::new(key, iv);
cipher
.apply_keystream_b2b(&plaintext, &mut conversion_buffer)
.unwrap();
assert_eq!(ciphertext, conversion_buffer);
let mut cipher = Aes256Ctr::new(key, iv);
cipher
.apply_keystream_b2b(&ciphertext, &mut conversion_buffer)
.unwrap();
assert_eq!(plaintext, conversion_buffer);
}
#[tokio::test]
async fn test_keys_parse() {
let mut parsed_keys = vec![];
let test_key_directory = format!("{}/tests/ssh_keys", env!("CARGO_MANIFEST_DIR"));
let mut directory_reader = tokio::fs::read_dir(test_key_directory)
.await
.expect("can read test key directory");
while let Ok(Some(entry)) = directory_reader.next_entry().await {
if entry.path().extension().is_none()
&& entry
.file_type()
.await
.map(|x| x.is_file())
.unwrap_or_default()
{
let mut private_keys = load_openssh_file_keys(entry.path(), &Some("hush".to_string()))
.await
.expect("can parse saved private key");
parsed_keys.append(&mut private_keys);
}
}
assert_eq!(18, parsed_keys.len());
}
#[tokio::test]
async fn improper_newline_errors_work() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
named_temp
.write_all("-----BEGIN OPENSSH PRIVATE KEY-----".as_bytes())
.unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::FileLackingNewlines
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
named_temp
.write_all(
"-----BEGIN OPENSSH PRIVATE KEY-----\n-----END OPENSSH PRIVATE KEY-----".as_bytes(),
)
.unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::FileLackingNewlines
));
}
#[tokio::test]
async fn improper_header_trailer_errors_work() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
named_temp
.write_all("-----BEGIN OPENSSH PRIVTE KEY-----\n".as_bytes())
.unwrap();
named_temp.write_all("stuff\n".as_bytes()).unwrap();
named_temp
.write_all("-----END OPENSSH PRIVATE KEY-----\n".as_bytes())
.unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::NoOpenSSLHeader
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
named_temp
.write_all("-----BEGIN OPENSSH PRIVATE KEY-----\n".as_bytes())
.unwrap();
named_temp.write_all("stuff\n".as_bytes()).unwrap();
named_temp
.write_all("-----END OPENSSH PRIVATEKEY-----\n".as_bytes())
.unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::NoOpenSSLTrailer
));
}
#[tokio::test]
async fn invalid_initial_data_errors_work() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(named_temp, "stuff").unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::Base64 { .. }
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(
named_temp,
"{}",
base64::engine::general_purpose::STANDARD.encode(b"openssl\x00")
)
.unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::NoMagicHeader
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(
named_temp,
"{}",
base64::engine::general_purpose::STANDARD.encode(b"openssl\xc3\x28key-v1\x00")
)
.unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::CouldNotDecodeMagicHeader { .. }
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(named_temp, "b3BlbnNzbC1rZXktdjFh").unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(result.unwrap_err().current_context(),
PrivateKeyLoadError::UnexpectedMagicHeaderValue { value }
if value == "openssl-key-v1a"));
}
#[test]
fn errors_on_weird_encryption_info() {
let mut example_bytes = SshReadBuffer::from(Bytes::from(b"\0\0\0\x09aes256ctr".to_vec()));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(
matches!(result.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { scheme }
if scheme == "aes256ctr")
);
let mut example_bytes = SshReadBuffer::from(Bytes::from(b"\0\0\0\x0aaes256-ctr".to_vec()));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::CouldNotDetermineKdf
));
let mut example_bytes =
SshReadBuffer::from(Bytes::from(b"\0\0\0\x0aaes256-ctr\0\0\0\x03foo".to_vec()));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(
matches!(result.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { scheme }
if scheme == "foo")
);
let mut example_bytes = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\0".to_vec(),
));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::CouldNotFindSaltBytes
));
let mut example_bytes = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x06\0\0\0\x04ab".to_vec(),
));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::CouldNotFindSaltBytes
));
let mut example_bytes = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x06\0\0\0\x02ab".to_vec(),
));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::CouldNotFindKdfRounds
));
let mut example_bytes = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x0b\0\0\0\x02ab\0\0\0\x02c".to_vec(),
));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::ExtraneousKdfInfo
));
}
#[test]
fn basic_kdf_examples_work() {
let mut no_encryption = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x04none\0\0\0\x04none\0\0\0\0".to_vec(),
));
let result = load_encryption_info(&mut no_encryption).unwrap();
assert_eq!(KeyEncryptionMode::None, result.encryption_mode);
assert!(matches!(
result.key_derivation_method,
KeyDerivationMethod::None
));
let mut encryption = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x0a\0\0\0\x02ab\0\0\0\x18".to_vec(),
));
let result = load_encryption_info(&mut encryption).unwrap();
assert_eq!(KeyEncryptionMode::Aes256Ctr, result.encryption_mode);
assert!(
matches!(result.key_derivation_method, KeyDerivationMethod::Bcrypt { salt, rounds }
if salt.as_ref() == b"ab" && rounds == 24)
);
}
#[cfg(test)]
pub fn generate_test_file(bytes: &[u8]) -> tempfile::TempPath {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(
named_temp,
"{}",
base64::engine::general_purpose::STANDARD.encode(bytes)
)
.unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
named_temp.into_temp_path()
}
#[tokio::test]
async fn invalid_encryption_info_stops_parsing() {
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x09aes256ctr");
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { .. }))
);
}
#[tokio::test]
async fn invalid_public_keys_stops_parsing() {
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0");
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotDeterminePublicKeyCount))
);
let path = generate_test_file(
b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\x01\0\0\0\x03foo",
);
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotLoadEncodedPublic))
);
}
#[tokio::test]
async fn checkint_validation_works() {
let path = generate_test_file(
b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x02ab",
);
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotGetCheckBytes { which } if which == &"first"))
);
let path = generate_test_file(
b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x06abcdef",
);
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotGetCheckBytes { which } if which == &"second"))
);
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x02");
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::DecryptionCheckError { checkint1, checkint2 } if *checkint1 == 1 && *checkint2 == 2))
);
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x01");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn padding_checks_work() {
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x01");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_ok());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x09\0\0\0\x01\0\0\0\x01\x01");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_ok());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x02");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_ok());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x09\0\0\0\x01\0\0\0\x01\x00");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_err());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x03");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_err());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x03\x04");
let result = load_openssh_file_keys(&path, &None).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyLoadError::ExtraneousData { amount } if
amount == &1)));
}
#[tokio::test]
async fn file_errors_are_caught() {
let result = load_openssh_file_keys("--capitan--", &None).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyLoadError::CouldNotRead { .. })));
}
#[tokio::test]
async fn mismatched_keys_are_handled() {
let test_path = format!(
"{}/tests/broken_keys/mismatched",
env!("CARGO_MANIFEST_DIR")
);
let result = load_openssh_file_keys(test_path.as_str(), &None).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyLoadError::MismatchedPublic)));
}
#[tokio::test]
async fn broken_private_info_strings_are_handled() {
let test_path = format!("{}/tests/broken_keys/bad_info", env!("CARGO_MANIFEST_DIR"));
let result = load_openssh_file_keys(test_path.as_str(), &None).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyLoadError::CouldNotLoadPrivateInfo)));
}

View File

@@ -0,0 +1,448 @@
use crate::crypto::rsa;
use crate::encodings::ssh::buffer::SshReadBuffer;
use bytes::Bytes;
use elliptic_curve::sec1::FromEncodedPoint;
use error_stack::{report, ResultExt};
use num_bigint_dig::BigUint;
use thiserror::Error;
/// An SSH public key type.
///
/// Technically, SSH supports additional key types not listed in this
/// enumeration, but we have chosen to be a little opinionated about
/// what constitutes a good key type. Note that SSH keys can also be
/// appended with additonal information in their various file types;
/// this code only processes the key material.
#[derive(Debug, PartialEq)]
pub enum PublicKey {
Rsa(rsa::PublicKey),
Ed25519(ed25519_dalek::VerifyingKey),
P256(p256::PublicKey),
P384(p384::PublicKey),
P521(p521::PublicKey),
}
impl PublicKey {
/// Returns the string that SSH would use to describe this key type.
///
/// It's not clear how standard these names are, but they are
/// associated with the output of OpenSSH, and appear to match
/// some of the strings listed in the SSH RFCs.
pub fn ssh_key_type_name(&self) -> &'static str {
match self {
PublicKey::Rsa(_) => "ssh-rsa",
PublicKey::P256(_) => "ecdsa-sha2-nistp256",
PublicKey::P384(_) => "ecdsa-sha2-nistp384",
PublicKey::P521(_) => "ecdsa-sha2-nistp521",
PublicKey::Ed25519(_) => "ssh-ed25519",
}
}
}
/// Errors that can occur trying to read an SSH public key from a
/// binary blob.
#[derive(Debug, Error)]
pub enum PublicKeyReadError {
#[error("Could not read encoded public key type")]
NoPublicKeyType,
#[error("Unrecognized encoded public key type: {key_type}")]
UnrecognizedKeyType { key_type: String },
#[error("Could not determine RSA public '{constant_name}' constant")]
CouldNotFindRsaConstant { constant_name: &'static str },
#[error("Extraneous information at the end of public '{key_type}' key")]
ExtraneousInfo { key_type: &'static str },
#[error("Could not find ed25519 public point")]
NoEd25519Data,
#[error("Invalid ed25519 public key value: {error}")]
InvalidEd25519Data {
error: ed25519_dalek::SignatureError,
},
#[error("Could not read ECDSA curve information")]
CouldNotReadCurve,
#[error(
"Mismatched ECDSA curve info in public key, saw key type {key_type} but curve {curve}"
)]
MismatchedCurveInfo { key_type: String, curve: String },
#[error("Could not read public {curve} point data")]
CouldNotReadEcdsaPoint { curve: String },
#[error("Invalid ECDSA point for curve {curve}: {error}")]
InvalidEcdsaPoint {
curve: String,
error: elliptic_curve::Error,
},
#[error("Invalid ECDSA public value for curve {curve}")]
InvalidEcdsaPublicValue { curve: String },
}
impl TryFrom<Bytes> for PublicKey {
type Error = error_stack::Report<PublicKeyReadError>;
fn try_from(value: Bytes) -> Result<Self, Self::Error> {
let mut ssh_buffer = SshReadBuffer::from(value);
let encoded_key_type = ssh_buffer
.get_string()
.change_context(PublicKeyReadError::NoPublicKeyType)?;
match encoded_key_type.as_str() {
"ssh-rsa" => {
let ebytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PublicKeyReadError::CouldNotFindRsaConstant { constant_name: "e" }
})?;
let nbytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PublicKeyReadError::CouldNotFindRsaConstant { constant_name: "n" }
})?;
if ssh_buffer.has_remaining() {
return Err(report!(PublicKeyReadError::ExtraneousInfo {
key_type: "RSA"
}));
}
let e = BigUint::from_bytes_be(&ebytes);
let n = BigUint::from_bytes_be(&nbytes);
Ok(PublicKey::Rsa(rsa::PublicKey::new(n, e)))
}
"ssh-ed25519" => {
let point_bytes = ssh_buffer
.get_bytes()
.change_context(PublicKeyReadError::NoEd25519Data)?;
if ssh_buffer.has_remaining() {
return Err(report!(PublicKeyReadError::ExtraneousInfo {
key_type: "ed25519"
}));
}
let point = ed25519_dalek::VerifyingKey::try_from(point_bytes.as_ref())
.map_err(|error| report!(PublicKeyReadError::InvalidEd25519Data { error }))?;
Ok(PublicKey::Ed25519(point))
}
"ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" => {
let curve = ssh_buffer
.get_string()
.change_context(PublicKeyReadError::CouldNotReadCurve)?;
match (encoded_key_type.as_str(), curve.as_str()) {
("ecdsa-sha2-nistp256", "nistp256") => {}
("ecdsa-sha2-nistp384", "nistp384") => {}
("ecdsa-sha2-nistp521", "nistp521") => {}
_ => {
return Err(report!(PublicKeyReadError::MismatchedCurveInfo {
key_type: encoded_key_type,
curve,
}))
}
}
let encoded_point_bytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PublicKeyReadError::CouldNotReadEcdsaPoint {
curve: curve.clone(),
}
})?;
match curve.as_str() {
"nistp256" => {
let point = p256::EncodedPoint::from_bytes(&encoded_point_bytes).map_err(
|error| {
report!(PublicKeyReadError::InvalidEcdsaPoint {
curve: curve.clone(),
error: error.into()
})
},
)?;
let public = p256::PublicKey::from_encoded_point(&point)
.into_option()
.ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue {
curve: curve.clone(),
})?;
Ok(PublicKey::P256(public))
}
"nistp384" => {
let point = p384::EncodedPoint::from_bytes(&encoded_point_bytes).map_err(
|error| {
report!(PublicKeyReadError::InvalidEcdsaPoint {
curve: curve.clone(),
error: error.into()
})
},
)?;
let public = p384::PublicKey::from_encoded_point(&point)
.into_option()
.ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue {
curve: curve.clone(),
})?;
Ok(PublicKey::P384(public))
}
"nistp521" => {
let point = p521::EncodedPoint::from_bytes(&encoded_point_bytes).map_err(
|error| {
report!(PublicKeyReadError::InvalidEcdsaPoint {
curve: curve.clone(),
error: error.into()
})
},
)?;
let public = p521::PublicKey::from_encoded_point(&point)
.into_option()
.ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue {
curve: curve.clone(),
})?;
Ok(PublicKey::P521(public))
}
_ => panic!(
"Should not be able to have a mismatched curve, but have {}",
curve
),
}
}
_ => Err(report!(PublicKeyReadError::UnrecognizedKeyType {
key_type: encoded_key_type
})),
}
}
}
#[test]
fn short_invalid_buffer_fails_correctly() {
let buffer = Bytes::from(vec![0, 1]);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::NoPublicKeyType
));
let buffer = Bytes::from(b"\x00\x00\x00\x05hippo".to_vec());
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), &PublicKeyReadError::UnrecognizedKeyType { ref key_type } if key_type.as_str() == "hippo")
);
}
#[cfg(test)]
const ECDSA256_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x41, 0x04, 0xd7, 0x47, 0x00, 0x93, 0x35, 0xc5, 0x88, 0xc1,
0x67, 0xb5, 0x1d, 0x5f, 0xf1, 0x9b, 0x82, 0x1d, 0xe8, 0x37, 0x21, 0xe7, 0x89, 0xe5, 0x7c, 0x14,
0x6a, 0xd7, 0xfe, 0x43, 0x44, 0xe7, 0x67, 0xd8, 0x05, 0x66, 0xe1, 0x96, 0x12, 0x8f, 0xc9, 0x23,
0x1c, 0x8f, 0x25, 0x0e, 0xa7, 0xf1, 0xcd, 0x76, 0x7a, 0xea, 0xb7, 0x87, 0x24, 0x07, 0x1e, 0x72,
0x63, 0x6b, 0x81, 0xde, 0x20, 0x81, 0xe7, 0x82,
];
#[cfg(test)]
const ECDSA384_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x61, 0x04, 0x0d, 0xa3, 0x8b, 0x42, 0x98, 0x15, 0xba, 0x0c,
0x9b, 0xf6, 0x5e, 0xc8, 0x68, 0xc3, 0x1e, 0x44, 0xb2, 0x6f, 0x12, 0x2f, 0xc8, 0x97, 0x81, 0x23,
0x60, 0xa0, 0xc3, 0xaf, 0xf1, 0x3f, 0x5f, 0xd6, 0xea, 0x49, 0x9c, 0xd6, 0x74, 0x34, 0xd0, 0x6a,
0xd0, 0x34, 0xe4, 0xd8, 0x42, 0x00, 0x94, 0x61, 0x63, 0x15, 0x11, 0xb0, 0x63, 0x52, 0xcc, 0xbe,
0xe5, 0xc2, 0x12, 0x33, 0xdc, 0x36, 0x03, 0x60, 0x6c, 0xb9, 0x11, 0xa6, 0xe4, 0x81, 0x64, 0x4a,
0x54, 0x74, 0x2b, 0xfb, 0xbc, 0xff, 0x90, 0xe0, 0x2c, 0x00, 0xc1, 0xae, 0x99, 0x2e, 0x0f, 0xdb,
0x50, 0xec, 0x4c, 0xe8, 0xbd, 0xf1, 0x0f, 0xdc,
];
#[cfg(test)]
const ECDSA521_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x85, 0x04, 0x01, 0x68, 0x9a, 0x37, 0xac, 0xa3, 0x16, 0x26,
0xa4, 0xaa, 0x72, 0xe6, 0x24, 0x40, 0x4c, 0x69, 0xbf, 0x11, 0x9e, 0xcd, 0xb6, 0x63, 0x92, 0x10,
0xa6, 0xb7, 0x6e, 0x98, 0xb4, 0xa0, 0x81, 0xc5, 0x3c, 0x88, 0xfa, 0x9b, 0x60, 0x57, 0x4c, 0x0f,
0xba, 0x36, 0x4e, 0xc6, 0xe0, 0x3e, 0xa5, 0x86, 0x3d, 0xd3, 0xd5, 0x86, 0x96, 0xe9, 0x4a, 0x1c,
0x0c, 0xe2, 0x70, 0xff, 0x1f, 0x79, 0x06, 0x5d, 0x52, 0x9a, 0x01, 0x2b, 0x87, 0x8e, 0xc2, 0xe9,
0xe2, 0xb7, 0x01, 0x00, 0xa6, 0x1a, 0xf7, 0x23, 0x47, 0x6a, 0x70, 0x10, 0x09, 0x59, 0xde, 0x0a,
0x20, 0xca, 0x2f, 0xd7, 0x5a, 0x98, 0xbd, 0xc3, 0x5b, 0xf2, 0x7b, 0x14, 0x6e, 0x6b, 0xa5, 0x93,
0x5d, 0x3e, 0x21, 0x5c, 0x49, 0x40, 0xbf, 0x9b, 0xc0, 0x78, 0x4b, 0xb1, 0xe9, 0xc7, 0x02, 0xb1,
0x51, 0x94, 0x1a, 0xcf, 0x88, 0x7b, 0xfe, 0xea, 0xd8, 0x55, 0x89, 0xb3,
];
#[test]
fn ecdsa_public_works() {
let buffer = Bytes::from(ECDSA256_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::P256(_)));
let buffer = Bytes::from(ECDSA384_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::P384(_)));
let buffer = Bytes::from(ECDSA521_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::P521(_)));
}
#[test]
fn checks_for_mismatched_curves() {
let mut raw_data = ECDSA256_TEST_KEY.to_vec();
raw_data[32] = 0x33;
raw_data[33] = 0x38;
raw_data[34] = 0x34;
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), PublicKeyReadError::MismatchedCurveInfo { ref key_type, ref curve }
if key_type.as_str() == "ecdsa-sha2-nistp256" && curve.as_str() == "nistp384")
);
}
#[test]
fn invalid_point_errors() {
let mut raw_data = ECDSA256_TEST_KEY.to_vec();
let _ = raw_data.pop().unwrap();
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), PublicKeyReadError::CouldNotReadEcdsaPoint { ref curve }
if curve.as_str() == "nistp256")
);
let mut raw_data = ECDSA256_TEST_KEY.to_vec();
raw_data[64] = 0x33;
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), PublicKeyReadError::InvalidEcdsaPublicValue { ref curve }
if curve.as_str() == "nistp256")
);
let mut raw_data = ECDSA256_TEST_KEY.to_vec();
raw_data[39] = 0x33;
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), PublicKeyReadError::InvalidEcdsaPoint { ref curve, .. }
if curve.as_str() == "nistp256")
);
}
#[cfg(test)]
const ED25519_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00,
0x00, 0x00, 0x20, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e, 0xda, 0x9b, 0x06, 0x3c,
0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda, 0xff, 0x85, 0x51, 0x15,
0x63, 0x58, 0xd3,
];
#[test]
fn ed25519_public_works() {
let buffer = Bytes::from(ED25519_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::Ed25519(_)));
}
#[test]
fn shortened_ed25519_fails() {
let buffer = Bytes::from(&ED25519_TEST_KEY[0..ED25519_TEST_KEY.len() - 2]);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::NoEd25519Data
));
}
#[test]
fn invalid_data_kills_ed25519_read() {
let mut raw_data = ED25519_TEST_KEY.to_vec();
raw_data[19] = 0x00;
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::InvalidEd25519Data { .. }
));
}
#[test]
fn extraneous_data_kills_ed25519_read() {
let mut raw_data = ED25519_TEST_KEY.to_vec();
raw_data.push(0);
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::ExtraneousInfo { .. }
));
}
#[cfg(test)]
const RSA_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61, 0x00, 0x00, 0x00, 0x03, 0x01,
0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0xb7, 0x7e, 0xd2, 0x53, 0xf0, 0x92, 0xac, 0x06, 0x53,
0x07, 0x8f, 0xe9, 0x89, 0xd8, 0x92, 0xd4, 0x08, 0x7e, 0xdd, 0x6b, 0xa4, 0x67, 0xd8, 0xac, 0x4a,
0x3b, 0x8f, 0xbd, 0x2f, 0x3a, 0x19, 0x46, 0x7c, 0xa5, 0x7f, 0xc1, 0x01, 0xee, 0xe3, 0xbf, 0x9e,
0xaf, 0xed, 0xc8, 0xbc, 0x8c, 0x30, 0x70, 0x6f, 0xf1, 0xdd, 0xb9, 0x9b, 0x4c, 0x67, 0x7b, 0x8f,
0x7c, 0xcf, 0x85, 0x6f, 0x28, 0x5f, 0xeb, 0xe3, 0x0b, 0x7f, 0x82, 0xf5, 0xa4, 0x99, 0xc6, 0xae,
0x1c, 0xbd, 0xd6, 0xa9, 0x34, 0xc9, 0x05, 0xfc, 0xdc, 0xe2, 0x84, 0x86, 0x69, 0xc5, 0x6b, 0x0a,
0xf5, 0x17, 0x5f, 0x52, 0xda, 0x4a, 0xdf, 0xd9, 0x4a, 0xe2, 0x14, 0x0c, 0xba, 0x96, 0x04, 0x4e,
0x25, 0x38, 0xd1, 0x66, 0x75, 0xf2, 0x27, 0x68, 0x1f, 0x28, 0xce, 0xa5, 0xa3, 0x22, 0x05, 0xf7,
0x9e, 0x38, 0x70, 0xf7, 0x23, 0x65, 0xfe, 0x4e, 0x77, 0x66, 0x70, 0x16, 0x89, 0xa3, 0xa7, 0x1b,
0xbd, 0x6d, 0x94, 0x85, 0xa1, 0x6b, 0xe8, 0xf1, 0xb9, 0xb6, 0x7f, 0x4f, 0xb4, 0x53, 0xa7, 0xfe,
0x2d, 0x89, 0x6a, 0x6e, 0x6d, 0x63, 0x85, 0xe1, 0x00, 0x83, 0x01, 0xb0, 0x00, 0x8a, 0x30, 0xde,
0xdc, 0x2f, 0x30, 0xbc, 0x89, 0x66, 0x2a, 0x28, 0x59, 0x31, 0xd9, 0x74, 0x9c, 0xf2, 0xf1, 0xd7,
0x53, 0xa9, 0x7b, 0xeb, 0x97, 0xfd, 0x53, 0x13, 0x66, 0x59, 0x9d, 0x61, 0x4a, 0x72, 0xf4, 0xa9,
0x22, 0xc8, 0xac, 0x0e, 0xd8, 0x0e, 0x4f, 0x15, 0x59, 0x9b, 0xaa, 0x96, 0xf9, 0xd5, 0x61, 0xd5,
0x04, 0x4c, 0x09, 0x0d, 0x5a, 0x4e, 0x39, 0xd6, 0xbe, 0x16, 0x8c, 0x36, 0xe1, 0x1d, 0x59, 0x5a,
0xa5, 0x5c, 0x50, 0x6b, 0x6f, 0x6a, 0xed, 0x63, 0x04, 0xbc, 0x42, 0xec, 0xcb, 0xea, 0x34, 0xfc,
0x75, 0xcc, 0xd1, 0xca, 0x45, 0x66, 0xd0, 0xc9, 0x14, 0xae, 0x83, 0xd0, 0x7c, 0x0e, 0x06, 0x1d,
0x4f, 0x15, 0x64, 0x53, 0x56, 0xdb, 0xf2, 0x49, 0x83, 0x03, 0xae, 0xda, 0xa7, 0x29, 0x7c, 0x42,
0xbf, 0x82, 0x07, 0xbc, 0x44, 0x09, 0x15, 0x32, 0x4d, 0xc0, 0xdf, 0x8a, 0x04, 0x89, 0xd9, 0xd8,
0xdb, 0x05, 0xa5, 0x60, 0x21, 0xed, 0xcb, 0x54, 0x74, 0x1e, 0x24, 0x06, 0x4d, 0x69, 0x93, 0x72,
0xe8, 0x59, 0xe1, 0x93, 0x1a, 0x6e, 0x48, 0x16, 0x31, 0x38, 0x10, 0x0e, 0x0b, 0x34, 0xeb, 0x20,
0x86, 0x9c, 0x60, 0x68, 0xaf, 0x30, 0x5e, 0x7f, 0x26, 0x37, 0xce, 0xd9, 0xc1, 0x47, 0xdf, 0x2d,
0xba, 0x50, 0x96, 0xcf, 0xf8, 0xf5, 0xe8, 0x65, 0x26, 0x18, 0x4a, 0x88, 0xe0, 0xd8, 0xab, 0x24,
0xde, 0x3f, 0xa9, 0x64, 0x94, 0xe3, 0xaf, 0x7b, 0x43, 0xaa, 0x72, 0x64, 0x7c, 0xef, 0xdb, 0x30,
0x87, 0x7d, 0x70, 0xd7, 0xbe, 0x0a, 0xca, 0x79, 0xe6, 0xb8, 0x3e, 0x23, 0x37, 0x17, 0x7d, 0x0c,
0x41, 0x3d, 0xd9, 0x92, 0xd6, 0x8c, 0x95, 0x8b, 0x63, 0x0b, 0x63, 0x49, 0x98, 0x0f, 0x1f, 0xc1,
0x95, 0x94, 0x6f, 0x22, 0x0e, 0x47, 0x8f, 0xee, 0x12, 0xb9, 0x8e, 0x28, 0xc2, 0x94, 0xa2, 0xd4,
0x0a, 0x79, 0x69, 0x93, 0x8a, 0x6f, 0xf4, 0xae, 0xd1, 0x85, 0x11, 0xbb, 0x6c, 0xd5, 0x41, 0x00,
0x71, 0x9b, 0x24, 0xe4, 0x6d, 0x0a, 0x05, 0x07, 0x4c, 0x28, 0xa6, 0x88, 0x8c, 0xea, 0x74, 0x19,
0x64, 0x26, 0x5a, 0xc8, 0x28, 0xcc, 0xdf, 0xa8, 0xea, 0xa7, 0xda, 0xec, 0x03, 0xcd, 0xcb, 0xf3,
0xd7, 0x6b, 0xb6, 0x4a, 0xd8, 0x50, 0x44, 0x91, 0xde, 0xb2, 0x76, 0x6e, 0x85, 0x21, 0x4b, 0x2f,
0x65, 0x57, 0x76, 0xd3, 0xd9, 0xfa, 0xd2, 0x98, 0xcb, 0x47, 0xaa, 0x33, 0x69, 0x4e, 0x83, 0x75,
0xfe, 0x8e, 0xac, 0x0a, 0xf6, 0xb6, 0xb7,
];
#[test]
fn rsa_public_works() {
let buffer = Bytes::from(RSA_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::Rsa(_)));
}
#[test]
fn rsa_requires_both_constants() {
let buffer = Bytes::from(&RSA_TEST_KEY[0..RSA_TEST_KEY.len() - 2]);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), &PublicKeyReadError::CouldNotFindRsaConstant { constant_name } if constant_name == "n")
);
let buffer = Bytes::from(&RSA_TEST_KEY[0..RSA_TEST_KEY.len() - 520]);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), &PublicKeyReadError::CouldNotFindRsaConstant { constant_name } if constant_name == "e")
);
}
#[test]
fn extraneous_data_kills_rsa_read() {
let mut raw_data = RSA_TEST_KEY.to_vec();
raw_data.push(0);
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::ExtraneousInfo { .. }
));
}

View File

@@ -0,0 +1,204 @@
use crate::encodings::ssh::public_key::PublicKey;
use base64::engine::{self, Engine};
use bytes::Bytes;
use error_stack::{report, ResultExt};
use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub enum PublicKeyLoadError {
#[error("Could not read file {file}: {error}")]
CouldNotRead {
file: String,
error: tokio::io::Error,
},
#[error("Base64 decoding error in {file}: {error}")]
Base64 {
file: String,
error: base64::DecodeError,
},
#[error("Could not find key type information in {file}")]
NoSshType { file: String },
#[error("Invalid public key material found in {file}")]
InvalidKeyMaterial { file: String },
#[error(
"Inconsistent key type found between stated type, {alleged}, and the encoded {found} key"
)]
InconsistentKeyType {
alleged: String,
found: &'static str,
},
}
impl PublicKey {
/// Loads one or more public keys from an OpenSSH-formatted file.
///
/// The given file should be created by `ssh-keygen` or similar from the
/// OpenSSL suite, or from Hush tooling. Other SSH implementations may use
/// other formats that are not understood by this function. This function
/// will work on public key files where the contents have been concatenated,
/// one per line.
///
/// Returns the key(s) loaded from the file, or an error if the file is in an
/// invalid format or contains a nonsensical crypto value. Each public key is
/// paired with the extra info that is appended at the end of the key, for
/// whatever use it might have.
pub async fn load<P: AsRef<Path>>(
path: P,
) -> error_stack::Result<Vec<(Self, String)>, PublicKeyLoadError> {
let mut results = vec![];
let path = path.as_ref();
let all_public_key_data = tokio::fs::read_to_string(path).await.map_err(|error| {
report!(PublicKeyLoadError::CouldNotRead {
file: path.to_path_buf().display().to_string(),
error,
})
})?;
for public_key_data in all_public_key_data.lines() {
let (alleged_key_type, rest) = public_key_data.split_once(' ').ok_or_else(|| {
report!(PublicKeyLoadError::NoSshType {
file: path.to_path_buf().display().to_string(),
})
})?;
let (key_material, info) = rest.split_once(' ').unwrap_or((rest, ""));
let key_material = engine::general_purpose::STANDARD
.decode(key_material)
.map_err(|de| {
report!(PublicKeyLoadError::Base64 {
file: path.to_path_buf().display().to_string(),
error: de,
})
})?;
let key_material = Bytes::from(key_material);
let public_key = PublicKey::try_from(key_material).change_context_lazy(|| {
PublicKeyLoadError::InvalidKeyMaterial {
file: path.to_path_buf().display().to_string(),
}
})?;
if alleged_key_type != public_key.ssh_key_type_name() {
return Err(report!(PublicKeyLoadError::InconsistentKeyType {
alleged: alleged_key_type.to_string(),
found: public_key.ssh_key_type_name(),
}));
}
results.push((public_key, info.to_string()));
}
Ok(results)
}
}
#[tokio::test]
async fn test_keys_parse() {
let mut parsed_keys = vec![];
let test_key_directory = format!("{}/tests/ssh_keys", env!("CARGO_MANIFEST_DIR"));
let mut directory_reader = tokio::fs::read_dir(test_key_directory)
.await
.expect("can read test key directory");
while let Ok(Some(entry)) = directory_reader.next_entry().await {
if matches!(entry.path().extension(), Some(ext) if ext == "pub") {
let mut public_keys = PublicKey::load(entry.path())
.await
.expect("can parse saved public key");
parsed_keys.append(&mut public_keys);
}
}
assert_eq!(18, parsed_keys.len());
}
#[tokio::test]
async fn concatenated_keys_parse() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
let ecdsa1 = tokio::fs::read(format!(
"{}/tests/ssh_keys/ecdsa1.pub",
env!("CARGO_MANIFEST_DIR")
))
.await
.unwrap();
named_temp.write_all(&ecdsa1).unwrap();
let ecdsa2 = tokio::fs::read(format!(
"{}/tests/ssh_keys/ecdsa2.pub",
env!("CARGO_MANIFEST_DIR")
))
.await
.unwrap();
named_temp.write_all(&ecdsa2).unwrap();
let path = named_temp.into_temp_path();
let parsed_keys = PublicKey::load(&path).await.unwrap();
assert_eq!(2, parsed_keys.len());
}
#[tokio::test]
async fn file_errors_are_caught() {
let result = PublicKey::load("--capitan--").await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::CouldNotRead { .. })));
}
#[tokio::test]
async fn key_file_must_have_space() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "foobar").unwrap();
let path = named_temp.into_temp_path();
let result = PublicKey::load(path).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::NoSshType { .. })));
}
#[tokio::test]
async fn key_file_must_have_valid_base64() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "ssh-ed25519 foobar").unwrap();
let path = named_temp.into_temp_path();
let result = PublicKey::load(path).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::Base64 { .. })));
}
#[tokio::test]
async fn checks_for_valid_key() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(
named_temp,
"ssh-ed25519 {}",
base64::engine::general_purpose::STANDARD.encode(b"foobar")
)
.unwrap();
let path = named_temp.into_temp_path();
let result = PublicKey::load(path).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::InvalidKeyMaterial { .. })));
}
#[tokio::test]
async fn mismatched_key_types_are_caught() {
use std::io::Write;
let test_rsa_key = format!("{}/tests/ssh_keys/rsa4096a.pub", env!("CARGO_MANIFEST_DIR"));
let rsa_key_contents = tokio::fs::read_to_string(test_rsa_key).await.unwrap();
let (_, rest) = rsa_key_contents.split_once(' ').unwrap();
println!("rest: {:?}", rest);
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
write!(named_temp, "ssh-ed25519 ").unwrap();
writeln!(named_temp, "{}", rest).unwrap();
let path = named_temp.into_temp_path();
let result = PublicKey::load(path).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::InconsistentKeyType { .. })));
}

9
src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod client;
pub mod config;
pub mod crypto;
pub mod encodings;
pub mod network;
mod operational_error;
pub mod ssh;
pub use operational_error::OperationalError;

1
src/network.rs Normal file
View File

@@ -0,0 +1 @@
pub mod host;

211
src/network/host.rs Normal file
View File

@@ -0,0 +1,211 @@
use error_stack::{report, ResultExt};
use futures::stream::{FuturesUnordered, StreamExt};
use hickory_resolver::error::ResolveError;
use hickory_resolver::name_server::ConnectionProvider;
use hickory_resolver::AsyncResolver;
use std::collections::HashSet;
use std::fmt;
use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use thiserror::Error;
use tokio::net::TcpStream;
pub enum Host {
IPv4(Ipv4Addr),
IPv6(Ipv6Addr),
Hostname(String),
}
#[derive(Debug, Error)]
pub enum HostParseError {
#[error("Could not parse IPv6 address {address:?}: {error}")]
CouldNotParseIPv6 {
address: String,
error: AddrParseError,
},
#[error("Invalid hostname {hostname:?}")]
InvalidHostname { hostname: String },
}
impl fmt::Display for Host {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Host::IPv4(x) => x.fmt(f),
Host::IPv6(x) => x.fmt(f),
Host::Hostname(x) => x.fmt(f),
}
}
}
impl FromStr for Host {
type Err = HostParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(addr) = Ipv4Addr::from_str(s) {
return Ok(Host::IPv4(addr));
}
if let Ok(addr) = Ipv6Addr::from_str(s) {
return Ok(Host::IPv6(addr));
}
if s.starts_with('[') && s.ends_with(']') {
match s.trim_start_matches('[').trim_end_matches(']').parse() {
Ok(x) => return Ok(Host::IPv6(x)),
Err(e) => {
return Err(HostParseError::CouldNotParseIPv6 {
address: s.to_string(),
error: e,
})
}
}
}
if hostname_validator::is_valid(s) {
return Ok(Host::Hostname(s.to_string()));
}
Err(HostParseError::InvalidHostname {
hostname: s.to_string(),
})
}
}
#[derive(Debug, Error)]
pub enum ConnectionError {
#[error("Failed to resolve host: {error}")]
ResolveError {
#[from]
error: ResolveError,
},
#[error("No valid IP addresses found")]
NoAddresses,
#[error("Error connecting to host: {error}")]
ConnectionError {
#[from]
error: std::io::Error,
},
}
impl Host {
/// Resolve this host address to a set of underlying IP addresses.
///
/// It is possible that the set of addresses provided may be empty, if the
/// address properly resolves (as in, we get a good DNS response) but there
/// are no relevant records for us to use for IPv4 or IPv6 connections. There
/// is also no guarantee that the host will have both IPv4 and IPv6 addresses,
/// so you may only see one or the other.
pub async fn resolve<P: ConnectionProvider>(
&self,
resolver: &AsyncResolver<P>,
) -> Result<HashSet<IpAddr>, ResolveError> {
match self {
Host::IPv4(addr) => Ok(HashSet::from([IpAddr::V4(*addr)])),
Host::IPv6(addr) => Ok(HashSet::from([IpAddr::V6(*addr)])),
Host::Hostname(name) => {
let resolve_result = resolver.lookup_ip(name).await?;
let possibilities = resolve_result.iter().collect();
Ok(possibilities)
}
}
}
/// Connect to this host and port.
///
/// This routine will attempt to connect to every address provided by the
/// resolver, and return the first successful connection. If all of the
/// connections fail, it will return the first error it receives. This routine
/// will also return an error if there are no addresses to connect to (which
/// can happen in cases in which [`Host::resolve`] would return an empty set.
pub async fn connect<P: ConnectionProvider>(
&self,
resolver: &AsyncResolver<P>,
port: u16,
) -> error_stack::Result<TcpStream, ConnectionError> {
let addresses = self
.resolve(resolver)
.await
.map_err(ConnectionError::from)
.attach_printable_lazy(|| format!("target address {}", self))?;
let mut connectors = FuturesUnordered::new();
for address in addresses.into_iter() {
let connect_future = TcpStream::connect(SocketAddr::new(address, port));
connectors.push(connect_future);
}
let mut error = None;
while let Some(result) = connectors.next().await {
match result {
Err(e) if error.is_none() => error = Some(e),
Err(_) => {}
Ok(v) => return Ok(v),
}
}
let final_error = if let Some(e) = error {
ConnectionError::ConnectionError { error: e }
} else {
ConnectionError::NoAddresses
};
Err(report!(final_error)).attach_printable_lazy(|| format!("target address {}", self))
}
}
#[test]
fn ip4_hosts_work() {
assert!(
matches!(Host::from_str("127.0.0.1"), Ok(Host::IPv4(addr)) if addr == Ipv4Addr::new(127, 0, 0, 1))
);
}
#[test]
fn bare_ip6_hosts_work() {
assert!(matches!(
Host::from_str("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
Ok(Host::IPv6(_))
));
assert!(matches!(Host::from_str("2001:db8::1"), Ok(Host::IPv6(_))));
assert!(matches!(Host::from_str("2001:DB8::1"), Ok(Host::IPv6(_))));
assert!(matches!(Host::from_str("::1"), Ok(Host::IPv6(_))));
assert!(matches!(Host::from_str("::"), Ok(Host::IPv6(_))));
}
#[test]
fn wrapped_ip6_hosts_work() {
assert!(matches!(
Host::from_str("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"),
Ok(Host::IPv6(_))
));
assert!(matches!(Host::from_str("[2001:db8::1]"), Ok(Host::IPv6(_))));
assert!(matches!(Host::from_str("[2001:DB8::1]"), Ok(Host::IPv6(_))));
assert!(matches!(Host::from_str("[::1]"), Ok(Host::IPv6(_))));
assert!(matches!(Host::from_str("[::]"), Ok(Host::IPv6(_))));
}
#[test]
fn valid_domains_work() {
assert!(matches!(
Host::from_str("uhsure.com"),
Ok(Host::Hostname(_))
));
assert!(matches!(
Host::from_str("www.cs.indiana.edu"),
Ok(Host::Hostname(_))
));
}
#[test]
fn invalid_inputs_fail() {
assert!(matches!(
Host::from_str("[uhsure.com]"),
Err(HostParseError::CouldNotParseIPv6 { .. })
));
assert!(matches!(
Host::from_str("-uhsure.com"),
Err(HostParseError::InvalidHostname { .. })
));
}

47
src/operational_error.rs Normal file
View File

@@ -0,0 +1,47 @@
use crate::ssh::{SshKeyExchangeProcessingError, SshMessageID};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum OperationalError {
#[error("Configuration error")]
ConfigurationError,
#[error("Failed to connect to target address")]
Connection,
#[error("Failed to complete initial read: {0}")]
InitialRead(std::io::Error),
#[error("SSH banner was not formatted in UTF-8: {0}")]
BannerError(std::str::Utf8Error),
#[error("Invalid initial SSH versionling line: {line}")]
InvalidHeaderLine { line: String },
#[error("Error writing initial banner: {0}")]
WriteBanner(std::io::Error),
#[error("Unexpected disconnect from other side.")]
Disconnect,
#[error("{message} in unexpected place.")]
UnexpectedMessage { message: SshMessageID },
#[error("User authorization failed.")]
UserAuthFailed,
#[error("Request failed.")]
RequestFailed,
#[error("Failed to open channel.")]
OpenChannelFailure,
#[error("Other side closed connection.")]
OtherClosed,
#[error("Other side sent EOF.")]
OtherEof,
#[error("Channel failed.")]
ChannelFailure,
#[error("Error in initial handshake: {0}")]
KeyxProcessingError(#[from] SshKeyExchangeProcessingError),
#[error("Call into random number generator failed: {0}")]
RngFailure(#[from] rand::Error),
#[error("Invalid port number '{port_string}': {error}")]
InvalidPort {
port_string: String,
error: std::num::ParseIntError,
},
#[error("Invalid hostname '{0}'")]
InvalidHostname(String),
#[error("Unable to parse host address")]
UnableToParseHostAddress,
}

8
src/ssh.rs Normal file
View File

@@ -0,0 +1,8 @@
mod channel;
mod message_ids;
mod packets;
mod preamble;
pub use message_ids::SshMessageID;
pub use packets::SshKeyExchangeProcessingError;
pub use preamble::Preamble;

428
src/ssh/channel.rs Normal file
View File

@@ -0,0 +1,428 @@
use bytes::{BufMut, Bytes, BytesMut};
use proptest::arbitrary::Arbitrary;
use proptest::strategy::{BoxedStrategy, Strategy};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
use tokio::sync::Mutex;
const MAX_BUFFER_SIZE: usize = 64 * 1024;
#[derive(Clone, Debug, PartialEq)]
pub struct SshPacket {
pub buffer: Bytes,
}
pub struct SshChannel<Stream> {
read_side: Mutex<ReadSide<Stream>>,
write_side: Mutex<WriteSide<Stream>>,
cipher_block_size: usize,
mac_length: usize,
channel_is_closed: bool,
}
struct ReadSide<Stream> {
stream: ReadHalf<Stream>,
buffer: BytesMut,
}
struct WriteSide<Stream> {
stream: WriteHalf<Stream>,
buffer: BytesMut,
rng: ChaCha20Rng,
}
impl<Stream> SshChannel<Stream>
where
Stream: AsyncReadExt + AsyncWriteExt,
Stream: Send + Sync,
Stream: Unpin,
{
/// Create a new SSH channel.
///
/// SshChannels are designed to make sure the various SSH channel read /
/// write operations are cancel- and concurrency-safe. They take ownership
/// of the underlying stream once established, where "established" means
/// that we have read and written the initial SSH banners.
pub fn new(stream: Stream) -> SshChannel<Stream> {
let (read_half, write_half) = tokio::io::split(stream);
SshChannel {
read_side: Mutex::new(ReadSide {
stream: read_half,
buffer: BytesMut::with_capacity(MAX_BUFFER_SIZE),
}),
write_side: Mutex::new(WriteSide {
stream: write_half,
buffer: BytesMut::with_capacity(MAX_BUFFER_SIZE),
rng: ChaCha20Rng::from_entropy(),
}),
mac_length: 0,
cipher_block_size: 8,
channel_is_closed: false,
}
}
/// Read an SshPacket from the wire.
///
/// This function is cancel safe, and can be used in `select` (or similar)
/// without problems. It is also safe to be used in a multitasking setting.
/// Returns Ok(Some(...)) if a packet is found, or Ok(None) if we have
/// successfully reached the end of stream. It will also return Ok(None)
/// repeatedly after the stream is closed.
pub async fn read(&self) -> Result<Option<SshPacket>, std::io::Error> {
if self.channel_is_closed {
return Ok(None);
}
let mut reader = self.read_side.lock().await;
let mut local_buffer = vec![0; 4096];
// First, let's try to at least get a size in there.
while reader.buffer.len() < 5 {
let amt_read = reader.stream.read(&mut local_buffer).await?;
reader.buffer.extend_from_slice(&local_buffer[0..amt_read]);
}
let packet_size = ((reader.buffer[0] as usize) << 24)
| ((reader.buffer[1] as usize) << 16)
| ((reader.buffer[2] as usize) << 8)
| reader.buffer[3] as usize;
let padding_size = reader.buffer[4] as usize;
let total_size = 4 + 1 + packet_size + padding_size + self.mac_length;
tracing::trace!(
packet_size,
padding_size,
total_size,
"Initial packet information determined"
);
// Now we need to make sure that the buffer contains at least that
// many bytes. We do this transfer -- from the wire to an internal
// buffer -- to ensure cancel safety. If, at any point, this computation
// is cancelled, the lock will be released and the buffer will be in
// a reasonable place. A subsequent call should be able to pick up
// wherever we left off.
while reader.buffer.len() < total_size {
let amt_read = reader.stream.read(&mut local_buffer).await?;
reader.buffer.extend_from_slice(&local_buffer[0..amt_read]);
}
let mut new_packet = reader.buffer.split_to(total_size);
let _header = new_packet.split_to(5);
let payload = new_packet.split_to(total_size - padding_size - 5);
let _mac = new_packet.split_off(padding_size);
Ok(Some(SshPacket {
buffer: payload.freeze(),
}))
}
fn encode(&self, rng: &mut ChaCha20Rng, packet: SshPacket) -> Option<Bytes> {
let mut encoded_packet = BytesMut::new();
// Arbitrary-length padding, such that the total length of
// (packet_length || padding_length || payload || random padding)
// is a multiple of the cipher block size or 8, whichever is
// larger. There MUST be at least four bytes of padding. The
// padding SHOULD consist of random bytes. The maximum amount of
// padding is 255 bytes.
let paddingless_length = 4 + 1 + packet.buffer.len();
// the padding we need to get to an even multiple of the cipher
// block size, naturally jumping to the cipher block size if we're
// already aligned. (this is just easier, and since we can't have
// 0 as the final padding, seems reasonable to do.)
let mut rounded_padding =
self.cipher_block_size - (paddingless_length % self.cipher_block_size);
// now we enforce the must be greater than or equal to 4 rule
if rounded_padding < 4 {
rounded_padding += self.cipher_block_size;
}
// if this ends up being > 256, then we've run into something terrible
if rounded_padding > (u8::MAX as usize) {
tracing::error!(
payload_length = packet.buffer.len(),
cipher_block_size = ?self.cipher_block_size,
computed_padding = ?rounded_padding,
"generated incoherent padding value in write"
);
return None;
}
encoded_packet.put_u32(packet.buffer.len() as u32);
encoded_packet.put_u8(rounded_padding as u8);
encoded_packet.put(packet.buffer);
for _ in 0..rounded_padding {
encoded_packet.put_u8(rng.gen());
}
Some(encoded_packet.freeze())
}
/// Write an SshPacket to the wire.
///
/// This function is cancel safe, and can be used in `select` (or similar).
/// By cancel safe, we mean that one of the following outcomes is guaranteed
/// to occur if the operation is cancelled:
///
/// 1. The whole packet is written to the channel.
/// 2. No part of the packet is written to the channel.
/// 3. The channel is dead, and no further data can be written to it.
///
/// Note that this means that you cannot assume that the packet is not
/// written if the operation is cancelled, it just ensures that you will
/// not be in a place in which only part of the packet has been written.
pub async fn write(&self, packet: SshPacket) -> Result<(), std::io::Error> {
let mut final_data = { self.encode(&mut self.write_side.lock().await.rng, packet) };
loop {
if self.channel_is_closed {
return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
}
let mut writer = self.write_side.lock().await;
if let Some(bytes) = final_data.take() {
if bytes.len() + writer.buffer.len() < MAX_BUFFER_SIZE {
writer.buffer.put(bytes);
} else if !bytes.is_empty() {
final_data = Some(bytes);
}
}
let mut current_buffer = std::mem::take(&mut writer.buffer);
let _written = writer.stream.write_buf(&mut current_buffer).await?;
writer.buffer = current_buffer;
if writer.buffer.is_empty() && final_data.is_none() {
return Ok(());
}
}
}
}
impl Arbitrary for SshPacket {
type Parameters = bool;
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(start_with_real_message: Self::Parameters) -> Self::Strategy {
if start_with_real_message {
unimplemented!()
} else {
let data = proptest::collection::vec(u8::arbitrary(), 0..35000);
data.prop_map(|x| SshPacket { buffer: x.into() }).boxed()
}
}
}
#[cfg(test)]
#[derive(Debug, Clone, PartialEq)]
enum SendData {
Left(SshPacket),
Right(SshPacket),
}
#[cfg(test)]
impl Arbitrary for SendData {
type Parameters = <SshPacket as Arbitrary>::Parameters;
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
(bool::arbitrary(), SshPacket::arbitrary_with(args))
.prop_map(|(is_left, packet)| {
if is_left {
SendData::Left(packet)
} else {
SendData::Right(packet)
}
})
.boxed()
}
}
proptest::proptest! {
#[test]
fn can_read_back_anything(packet in SshPacket::arbitrary()) {
let result = tokio::runtime::Runtime::new().unwrap().block_on(async {
let (left, right) = tokio::io::duplex(8192);
let leftssh = SshChannel::new(left);
let rightssh = SshChannel::new(right);
let packet_copy = packet.clone();
tokio::task::spawn(async move {
leftssh.write(packet_copy).await.unwrap();
});
rightssh.read().await.unwrap()
});
assert_eq!(packet, result.unwrap());
}
#[test]
fn sequences_send_correctly_serial(sequence in proptest::collection::vec(SendData::arbitrary(), 0..100)) {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let (left, right) = tokio::io::duplex(8192);
let leftssh = SshChannel::new(left);
let rightssh = SshChannel::new(right);
let sequence_left = sequence.clone();
let sequence_right = sequence;
let left_task = tokio::task::spawn(async move {
let mut errored = false;
for item in sequence_left.into_iter() {
match item {
SendData::Left(packet) => {
let result = leftssh.write(packet).await;
errored = result.is_err();
}
SendData::Right(packet) => {
if let Ok(Some(item)) = leftssh.read().await {
errored = item != packet;
} else {
errored = true;
}
}
}
if errored {
break
}
}
!errored
});
let right_task = tokio::task::spawn(async move {
let mut errored = false;
for item in sequence_right.into_iter() {
match item {
SendData::Right(packet) => {
let result = rightssh.write(packet).await;
errored = result.is_err();
}
SendData::Left(packet) => {
if let Ok(Some(item)) = rightssh.read().await {
errored = item != packet;
} else {
errored = true;
}
}
}
if errored {
break
}
}
!errored
});
assert!(left_task.await.unwrap());
assert!(right_task.await.unwrap());
});
}
#[test]
fn sequences_send_correctly_parallel(sequence in proptest::collection::vec(SendData::arbitrary(), 0..100)) {
use std::sync::Arc;
tokio::runtime::Runtime::new().unwrap().block_on(async {
let (left, right) = tokio::io::duplex(8192);
let leftsshw = Arc::new(SshChannel::new(left));
let leftsshr = leftsshw.clone();
let rightsshw = Arc::new(SshChannel::new(right));
let rightsshr = rightsshw.clone();
let sequence_left_write = sequence.clone();
let sequence_left_read = sequence.clone();
let sequence_right_write = sequence.clone();
let sequence_right_read = sequence.clone();
let left_task_write = tokio::task::spawn(async move {
let mut errored = false;
for item in sequence_left_write.into_iter() {
if let SendData::Left(packet) = item {
let result = leftsshw.write(packet).await;
errored = result.is_err();
}
if errored {
break
}
}
!errored
});
let right_task_write = tokio::task::spawn(async move {
let mut errored = false;
for item in sequence_right_write.into_iter() {
if let SendData::Right(packet) = item {
let result = rightsshw.write(packet).await;
errored = result.is_err();
}
if errored {
break
}
}
!errored
});
let left_task_read = tokio::task::spawn(async move {
let mut errored = false;
for item in sequence_left_read.into_iter() {
if let SendData::Right(packet) = item {
if let Ok(Some(item)) = leftsshr.read().await {
errored = item != packet;
} else {
errored = true;
}
}
if errored {
break
}
}
!errored
});
let right_task_read = tokio::task::spawn(async move {
let mut errored = false;
for item in sequence_right_read.into_iter() {
if let SendData::Left(packet) = item {
if let Ok(Some(item)) = rightsshr.read().await {
errored = item != packet;
} else {
errored = true;
}
}
if errored {
break
}
}
!errored
});
assert!(left_task_write.await.unwrap());
assert!(right_task_write.await.unwrap());
assert!(left_task_read.await.unwrap());
assert!(right_task_read.await.unwrap());
});
}
}

173
src/ssh/message_ids.rs Normal file
View File

@@ -0,0 +1,173 @@
use crate::operational_error::OperationalError;
use num_enum::{FromPrimitive, IntoPrimitive};
use proptest::arbitrary::Arbitrary;
use proptest::strategy::{BoxedStrategy, Just, Strategy};
use std::fmt;
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum SshMessageID {
SSH_MSG_DISCONNECT = 1,
SSH_MSG_IGNORE = 2,
SSH_MSG_UNIMPLEMENTED = 3,
SSH_MSG_DEBUG = 4,
SSH_MSG_SERVICE_REQUEST = 5,
SSH_MSG_SERVICE_ACCEPT = 6,
SSH_MSG_KEXINIT = 20,
SSH_MSG_NEWKEYS = 21,
SSH_MSG_USERAUTH_REQUEST = 50,
SSH_MSG_USERAUTH_FAILURE = 51,
SSH_MSG_USERAUTH_SUCCESS = 52,
SSH_MSG_USERAUTH_BANNER = 53,
SSH_MSG_GLOBAL_REQUEST = 80,
SSH_MSG_REQUEST_SUCCESS = 81,
SSH_MSG_REQUEST_FAILURE = 82,
SSH_MSG_CHANNEL_OPEN = 90,
SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91,
SSH_MSG_CHANNEL_OPEN_FAILURE = 92,
SSH_MSG_CHANNEL_WINDOW_ADJUST = 93,
SSH_MSG_CHANNEL_DATA = 94,
SSH_MSG_CHANNEL_EXTENDED_DATA = 95,
SSH_MSG_CHANNEL_EOF = 96,
SSH_MSG_CHANNEL_CLOSE = 97,
SSH_MSG_CHANNEL_REQUEST = 98,
SSH_MSG_CHANNEL_SUCCESS = 99,
SSH_MSG_CHANNEL_FAILURE = 100,
#[num_enum(catch_all)]
Unknown(u8),
}
impl fmt::Display for SshMessageID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SshMessageID::SSH_MSG_DISCONNECT => write!(f, "SSH_MSG_DISCONNECT"),
SshMessageID::SSH_MSG_IGNORE => write!(f, "SSH_MSG_IGNORE"),
SshMessageID::SSH_MSG_UNIMPLEMENTED => write!(f, "SSH_MSG_UNIMPLEMENTED"),
SshMessageID::SSH_MSG_DEBUG => write!(f, "SSH_MSG_DEBUG"),
SshMessageID::SSH_MSG_SERVICE_REQUEST => write!(f, "SSH_MSG_SERVICE_REQUEST"),
SshMessageID::SSH_MSG_SERVICE_ACCEPT => write!(f, "SSH_MSG_SERVICE_ACCEPT"),
SshMessageID::SSH_MSG_KEXINIT => write!(f, "SSH_MSG_KEXINIT"),
SshMessageID::SSH_MSG_NEWKEYS => write!(f, "SSH_MSG_NEWKEYS"),
SshMessageID::SSH_MSG_USERAUTH_REQUEST => write!(f, "SSH_MSG_USERAUTH_REQUEST"),
SshMessageID::SSH_MSG_USERAUTH_FAILURE => write!(f, "SSH_MSG_USERAUTH_FAILURE"),
SshMessageID::SSH_MSG_USERAUTH_SUCCESS => write!(f, "SSH_MSG_USERAUTH_SUCCESS"),
SshMessageID::SSH_MSG_USERAUTH_BANNER => write!(f, "SSH_MSG_USERAUTH_BANNER"),
SshMessageID::SSH_MSG_GLOBAL_REQUEST => write!(f, "SSH_MSG_GLOBAL_REQUEST"),
SshMessageID::SSH_MSG_REQUEST_SUCCESS => write!(f, "SSH_MSG_REQUEST_SUCCESS"),
SshMessageID::SSH_MSG_REQUEST_FAILURE => write!(f, "SSH_MSG_REQUEST_FAILURE"),
SshMessageID::SSH_MSG_CHANNEL_OPEN => write!(f, "SSH_MSG_CHANNEL_OPEN"),
SshMessageID::SSH_MSG_CHANNEL_OPEN_CONFIRMATION => {
write!(f, "SSH_MSG_CHANNEL_OPEN_CONFIRMATION")
}
SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE => write!(f, "SSH_MSG_CHANNEL_OPEN_FAILURE"),
SshMessageID::SSH_MSG_CHANNEL_WINDOW_ADJUST => {
write!(f, "SSH_MSG_CHANNEL_WINDOW_ADJUST")
}
SshMessageID::SSH_MSG_CHANNEL_DATA => write!(f, "SSH_MSG_CHANNEL_DATA"),
SshMessageID::SSH_MSG_CHANNEL_EXTENDED_DATA => {
write!(f, "SSH_MSG_CHANNEL_EXTENDED_DATA")
}
SshMessageID::SSH_MSG_CHANNEL_EOF => write!(f, "SSH_MSG_CHANNEL_EOF"),
SshMessageID::SSH_MSG_CHANNEL_CLOSE => write!(f, "SSH_MSG_CHANNEL_CLOSE"),
SshMessageID::SSH_MSG_CHANNEL_REQUEST => write!(f, "SSH_MSG_CHANNEL_REQUEST"),
SshMessageID::SSH_MSG_CHANNEL_SUCCESS => write!(f, "SSH_MSG_CHANNEL_SUCCESS"),
SshMessageID::SSH_MSG_CHANNEL_FAILURE => write!(f, "SSH_MSG_CHANNEL_FAILURE"),
SshMessageID::Unknown(x) => write!(f, "SSH_MSG_UNKNOWN{}", x),
}
}
}
#[test]
fn no_duplicate_messages() {
let mut found = std::collections::HashSet::new();
for i in u8::MIN..=u8::MAX {
let id = SshMessageID::from_primitive(i);
let display = id.to_string();
assert!(!found.contains(&display));
found.insert(display);
}
}
impl From<SshMessageID> for OperationalError {
fn from(message: SshMessageID) -> Self {
match message {
SshMessageID::SSH_MSG_DISCONNECT => OperationalError::Disconnect,
SshMessageID::SSH_MSG_USERAUTH_FAILURE => OperationalError::UserAuthFailed,
SshMessageID::SSH_MSG_REQUEST_FAILURE => OperationalError::RequestFailed,
SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE => OperationalError::OpenChannelFailure,
SshMessageID::SSH_MSG_CHANNEL_EOF => OperationalError::OtherEof,
SshMessageID::SSH_MSG_CHANNEL_CLOSE => OperationalError::OtherClosed,
SshMessageID::SSH_MSG_CHANNEL_FAILURE => OperationalError::ChannelFailure,
_ => OperationalError::UnexpectedMessage { message },
}
}
}
impl TryFrom<OperationalError> for SshMessageID {
type Error = OperationalError;
fn try_from(value: OperationalError) -> Result<Self, Self::Error> {
match value {
OperationalError::Disconnect => Ok(SshMessageID::SSH_MSG_DISCONNECT),
OperationalError::UserAuthFailed => Ok(SshMessageID::SSH_MSG_USERAUTH_FAILURE),
OperationalError::RequestFailed => Ok(SshMessageID::SSH_MSG_REQUEST_FAILURE),
OperationalError::OpenChannelFailure => Ok(SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE),
OperationalError::OtherEof => Ok(SshMessageID::SSH_MSG_CHANNEL_EOF),
OperationalError::OtherClosed => Ok(SshMessageID::SSH_MSG_CHANNEL_CLOSE),
OperationalError::ChannelFailure => Ok(SshMessageID::SSH_MSG_CHANNEL_FAILURE),
OperationalError::UnexpectedMessage { message } => Ok(message),
_ => Err(value),
}
}
}
impl Arbitrary for SshMessageID {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
proptest::prop_oneof![
Just(SshMessageID::SSH_MSG_DISCONNECT),
Just(SshMessageID::SSH_MSG_IGNORE),
Just(SshMessageID::SSH_MSG_UNIMPLEMENTED),
Just(SshMessageID::SSH_MSG_DEBUG),
Just(SshMessageID::SSH_MSG_SERVICE_REQUEST),
Just(SshMessageID::SSH_MSG_SERVICE_ACCEPT),
Just(SshMessageID::SSH_MSG_KEXINIT),
Just(SshMessageID::SSH_MSG_NEWKEYS),
Just(SshMessageID::SSH_MSG_USERAUTH_REQUEST),
Just(SshMessageID::SSH_MSG_USERAUTH_FAILURE),
Just(SshMessageID::SSH_MSG_USERAUTH_SUCCESS),
Just(SshMessageID::SSH_MSG_USERAUTH_BANNER),
Just(SshMessageID::SSH_MSG_GLOBAL_REQUEST),
Just(SshMessageID::SSH_MSG_REQUEST_SUCCESS),
Just(SshMessageID::SSH_MSG_REQUEST_FAILURE),
Just(SshMessageID::SSH_MSG_CHANNEL_OPEN),
Just(SshMessageID::SSH_MSG_CHANNEL_OPEN_CONFIRMATION),
Just(SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE),
Just(SshMessageID::SSH_MSG_CHANNEL_WINDOW_ADJUST),
Just(SshMessageID::SSH_MSG_CHANNEL_DATA),
Just(SshMessageID::SSH_MSG_CHANNEL_EXTENDED_DATA),
Just(SshMessageID::SSH_MSG_CHANNEL_EOF),
Just(SshMessageID::SSH_MSG_CHANNEL_CLOSE),
Just(SshMessageID::SSH_MSG_CHANNEL_REQUEST),
Just(SshMessageID::SSH_MSG_CHANNEL_SUCCESS),
Just(SshMessageID::SSH_MSG_CHANNEL_FAILURE),
]
.boxed()
}
}
proptest::proptest! {
#[test]
fn error_encodings_invert(message in SshMessageID::arbitrary()) {
let error_version = OperationalError::from(message);
let back_to_message = SshMessageID::try_from(error_version).unwrap();
assert_eq!(message, back_to_message);
}
}

3
src/ssh/packets.rs Normal file
View File

@@ -0,0 +1,3 @@
mod key_exchange;
pub use key_exchange::SshKeyExchangeProcessingError;

View File

@@ -0,0 +1,332 @@
use crate::config::ClientConnectionOpts;
use crate::ssh::channel::SshPacket;
use crate::ssh::message_ids::SshMessageID;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use itertools::Itertools;
use proptest::arbitrary::Arbitrary;
use proptest::strategy::{BoxedStrategy, Strategy};
use rand::{CryptoRng, Rng, SeedableRng};
use std::string::FromUtf8Error;
use thiserror::Error;
#[derive(Clone, Debug, PartialEq)]
pub struct SshKeyExchange {
cookie: [u8; 16],
keyx_algorithms: Vec<String>,
server_host_key_algorithms: Vec<String>,
encryption_algorithms_client_to_server: Vec<String>,
encryption_algorithms_server_to_client: Vec<String>,
mac_algorithms_client_to_server: Vec<String>,
mac_algorithms_server_to_client: Vec<String>,
compression_algorithms_client_to_server: Vec<String>,
compression_algorithms_server_to_client: Vec<String>,
languages_client_to_server: Vec<String>,
languages_server_to_client: Vec<String>,
first_kex_packet_follows: bool,
}
#[derive(Debug, Error)]
pub enum SshKeyExchangeProcessingError {
#[error("Message not appropriately tagged as SSH_MSG_KEXINIT")]
TaggedWrong,
#[error("Initial key exchange message was too short.")]
TooShort,
#[error("Invalid string encoding for name-list (not ASCII)")]
NotAscii,
#[error("Invalid conversion (from ASCII to UTF-8??): {0}")]
NotUtf8(FromUtf8Error),
#[error("Extraneous data at the end of key exchange message")]
ExtraneousData,
#[error("Received invalid reserved word ({0} != 0)")]
InvalidReservedWord(u32),
}
impl TryFrom<SshPacket> for SshKeyExchange {
type Error = SshKeyExchangeProcessingError;
fn try_from(mut value: SshPacket) -> Result<Self, Self::Error> {
if SshMessageID::from(value.buffer.get_u8()) != SshMessageID::SSH_MSG_KEXINIT {
return Err(SshKeyExchangeProcessingError::TaggedWrong);
}
let mut cookie = [0; 16];
check_length(&mut value.buffer, 16)?;
value.buffer.copy_to_slice(&mut cookie);
let keyx_algorithms = name_list(&mut value.buffer)?;
let server_host_key_algorithms = name_list(&mut value.buffer)?;
let encryption_algorithms_client_to_server = name_list(&mut value.buffer)?;
let encryption_algorithms_server_to_client = name_list(&mut value.buffer)?;
let mac_algorithms_client_to_server = name_list(&mut value.buffer)?;
let mac_algorithms_server_to_client = name_list(&mut value.buffer)?;
let compression_algorithms_client_to_server = name_list(&mut value.buffer)?;
let compression_algorithms_server_to_client = name_list(&mut value.buffer)?;
let languages_client_to_server = name_list(&mut value.buffer)?;
let languages_server_to_client = name_list(&mut value.buffer)?;
check_length(&mut value.buffer, 5)?;
let first_kex_packet_follows = value.buffer.get_u8() != 0;
let reserved = value.buffer.get_u32();
if reserved != 0 {
return Err(SshKeyExchangeProcessingError::InvalidReservedWord(reserved));
}
if value.buffer.remaining() > 0 {
return Err(SshKeyExchangeProcessingError::ExtraneousData);
}
Ok(SshKeyExchange {
cookie,
keyx_algorithms,
server_host_key_algorithms,
encryption_algorithms_client_to_server,
encryption_algorithms_server_to_client,
mac_algorithms_client_to_server,
mac_algorithms_server_to_client,
compression_algorithms_client_to_server,
compression_algorithms_server_to_client,
languages_client_to_server,
languages_server_to_client,
first_kex_packet_follows,
})
}
}
impl From<SshKeyExchange> for SshPacket {
fn from(value: SshKeyExchange) -> Self {
let mut buffer = BytesMut::new();
let put_options = |buffer: &mut BytesMut, vals: Vec<String>| {
let mut merged = String::new();
#[allow(unstable_name_collisions)]
let comma_sepped = vals.into_iter().intersperse(String::from(","));
merged.extend(comma_sepped);
let bytes = merged.as_bytes();
buffer.put_u32(bytes.len() as u32);
buffer.put_slice(bytes);
};
buffer.put_u8(SshMessageID::SSH_MSG_KEXINIT.into());
buffer.put_slice(&value.cookie);
put_options(&mut buffer, value.keyx_algorithms);
put_options(&mut buffer, value.server_host_key_algorithms);
put_options(&mut buffer, value.encryption_algorithms_client_to_server);
put_options(&mut buffer, value.encryption_algorithms_server_to_client);
put_options(&mut buffer, value.mac_algorithms_client_to_server);
put_options(&mut buffer, value.mac_algorithms_server_to_client);
put_options(&mut buffer, value.compression_algorithms_client_to_server);
put_options(&mut buffer, value.compression_algorithms_server_to_client);
put_options(&mut buffer, value.languages_client_to_server);
put_options(&mut buffer, value.languages_server_to_client);
buffer.put_u8(value.first_kex_packet_follows as u8);
buffer.put_u32(0);
SshPacket {
buffer: buffer.freeze(),
}
}
}
impl SshKeyExchange {
/// Create a new SshKeyExchange message for this client or server based
/// on the given connection options.
///
/// This function takes a random number generator because it needs to
/// seed the message with a random cookie, but is otherwise deterministic.
/// It will fail only in the case that the underlying random number
/// generator fails, and return exactly that error.
pub fn new<R>(rng: &mut R, value: ClientConnectionOpts) -> Result<Self, rand::Error>
where
R: CryptoRng + Rng,
{
let mut result = SshKeyExchange {
cookie: [0; 16],
keyx_algorithms: value
.key_exchange_algorithms
.iter()
.map(|x| x.to_string())
.collect(),
server_host_key_algorithms: value
.server_host_key_algorithms
.iter()
.map(|x| x.to_string())
.collect(),
encryption_algorithms_client_to_server: value
.encryption_algorithms
.iter()
.map(|x| x.to_string())
.collect(),
encryption_algorithms_server_to_client: value
.encryption_algorithms
.iter()
.map(|x| x.to_string())
.collect(),
mac_algorithms_client_to_server: value
.mac_algorithms
.iter()
.map(|x| x.to_string())
.collect(),
mac_algorithms_server_to_client: value
.mac_algorithms
.iter()
.map(|x| x.to_string())
.collect(),
compression_algorithms_client_to_server: value
.compression_algorithms
.iter()
.map(|x| x.to_string())
.collect(),
compression_algorithms_server_to_client: value
.compression_algorithms
.iter()
.map(|x| x.to_string())
.collect(),
languages_client_to_server: value.languages.to_vec(),
languages_server_to_client: value.languages.to_vec(),
first_kex_packet_follows: value.predict.is_some(),
};
rng.try_fill(&mut result.cookie)?;
Ok(result)
}
}
impl Arbitrary for SshKeyExchange {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> BoxedStrategy<Self> {
let client_config = ClientConnectionOpts::arbitrary();
let seed = <[u8; 32]>::arbitrary();
(client_config, seed)
.prop_map(|(config, seed)| {
let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed);
SshKeyExchange::new(&mut rng, config).unwrap()
})
.boxed()
}
}
proptest::proptest! {
#[test]
fn valid_kex_messages_parse(kex in SshKeyExchange::arbitrary()) {
let as_packet: SshPacket = kex.clone().try_into().expect("can generate packet");
let as_message = as_packet.try_into().expect("can regenerate message");
assert_eq!(kex, as_message);
}
}
fn check_length(buffer: &mut Bytes, length: usize) -> Result<(), SshKeyExchangeProcessingError> {
if buffer.remaining() < length {
Err(SshKeyExchangeProcessingError::TooShort)
} else {
Ok(())
}
}
fn name_list(buffer: &mut Bytes) -> Result<Vec<String>, SshKeyExchangeProcessingError> {
check_length(buffer, 4)?;
let list_length = buffer.get_u32() as usize;
if list_length == 0 {
return Ok(vec![]);
}
check_length(buffer, list_length)?;
let mut raw_bytes = vec![0u8; list_length];
buffer.copy_to_slice(&mut raw_bytes);
if !raw_bytes.iter().all(|c| c.is_ascii()) {
return Err(SshKeyExchangeProcessingError::NotAscii);
}
let mut result = Vec::new();
for split in raw_bytes.split(|b| char::from(*b) == ',') {
let str =
String::from_utf8(split.to_vec()).map_err(SshKeyExchangeProcessingError::NotUtf8)?;
result.push(str);
}
Ok(result)
}
#[cfg(test)]
fn standard_kex_message() -> SshPacket {
let seed = [0u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed);
let config = ClientConnectionOpts::default();
let message = SshKeyExchange::new(&mut rng, config).expect("default settings work");
message.try_into().expect("default settings serialize")
}
#[test]
fn can_see_bad_tag() {
let standard = standard_kex_message();
let mut buffer = standard.buffer.to_vec();
buffer[0] += 1;
let bad = SshPacket {
buffer: buffer.into(),
};
assert!(matches!(
SshKeyExchange::try_from(bad),
Err(SshKeyExchangeProcessingError::TaggedWrong)
));
}
#[test]
fn checks_for_extraneous_data() {
let standard = standard_kex_message();
let mut buffer = standard.buffer.to_vec();
buffer.push(3);
let bad = SshPacket {
buffer: buffer.into(),
};
assert!(matches!(
SshKeyExchange::try_from(bad),
Err(SshKeyExchangeProcessingError::ExtraneousData)
));
}
#[test]
fn checks_for_short_packets() {
let standard = standard_kex_message();
let mut buffer = standard.buffer.to_vec();
let _ = buffer.pop();
let bad = SshPacket {
buffer: buffer.into(),
};
assert!(matches!(
SshKeyExchange::try_from(bad),
Err(SshKeyExchangeProcessingError::TooShort)
));
}
#[test]
fn checks_for_invalid_data() {
let standard = standard_kex_message();
let mut buffer = standard.buffer.to_vec();
buffer[22] = 0xc3;
buffer[23] = 0x28;
let bad = SshPacket {
buffer: buffer.into(),
};
assert!(matches!(
SshKeyExchange::try_from(bad),
Err(SshKeyExchangeProcessingError::NotAscii)
));
}
#[test]
fn checks_for_bad_reserved_word() {
let standard = standard_kex_message();
let mut buffer = standard.buffer.to_vec();
let _ = buffer.pop();
buffer.push(1);
let bad = SshPacket {
buffer: buffer.into(),
};
assert!(matches!(
SshKeyExchange::try_from(bad),
Err(SshKeyExchangeProcessingError::InvalidReservedWord(1))
));
}

392
src/ssh/preamble.rs Normal file
View File

@@ -0,0 +1,392 @@
use error_stack::{report, ResultExt};
use proptest::arbitrary::Arbitrary;
use proptest::strategy::{BoxedStrategy, Strategy};
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, PartialEq)]
pub struct Preamble {
preamble: String,
software_name: String,
software_version: String,
commentary: String,
}
impl Arbitrary for Preamble {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
let name = proptest::string::string_regex("[[:alpha:]][[:alnum:]]{0,32}").unwrap();
let soft_major = u8::arbitrary();
let soft_minor = u8::arbitrary();
let soft_patch = proptest::option::of(u8::arbitrary());
let commentary = proptest::option::of(
proptest::string::string_regex("[[:alnum:]][[[:alnum:]][[:blank:]][[:punct:]]]{0,64}")
.unwrap(),
);
(name, soft_major, soft_minor, soft_patch, commentary)
.prop_map(|(name, major, minor, patch, commentary)| Preamble {
preamble: String::new(),
software_name: name,
software_version: if let Some(patch) = patch {
format!("{}.{}.{}", major, minor, patch)
} else {
format!("{}.{}", major, minor)
},
commentary: commentary.unwrap_or_default(),
})
.boxed()
}
}
impl Default for Preamble {
fn default() -> Self {
Preamble {
preamble: String::new(),
software_name: env!("CARGO_PKG_NAME").to_string(),
software_version: env!("CARGO_PKG_VERSION").to_string(),
commentary: String::new(),
}
}
}
#[derive(Debug, Error)]
pub enum PreambleReadError {
#[error("Reading from the input stream failed")]
Read,
#[error("Illegal version number, expected '2.0' (saw characters {0})")]
IllegalVersion(String),
#[error("No dash found after seeing SSH version number")]
NoDashAfterVersion,
#[error("Illegal character in SSH software name")]
IllegalSoftwareNameChar,
#[error("Protocol error in preamble: No line feed for carriage return")]
NoLineFeedForCarriage,
#[error("Missing the final newline in the preamble")]
MissingFinalNewline,
#[error("Illegal UTF-8 in software name")]
InvalidSoftwareName,
}
#[derive(Debug, Error)]
pub enum PreambleWriteError {
#[error("Could not write preamble to socket")]
Write,
}
#[derive(Debug)]
enum PreambleState {
StartOfLine,
Preamble,
CarriageReturn,
InitialS,
SecondS,
InitialH,
InitialDash,
Version2,
VersionDot,
Version0,
VersionDash,
SoftwareName,
SoftwareVersion,
Commentary,
FinalCarriageReturn,
}
impl Preamble {
/// Read an SSH preamble from the given read channel.
///
/// Will fail if the underlying read channel fails, or if the preamble does not
/// meet the formatting requirements of the RFC.
pub async fn read<R: AsyncReadExt + Unpin>(
connection: &mut R,
) -> error_stack::Result<Preamble, PreambleReadError> {
let mut preamble = String::new();
let mut software_name_bytes = Vec::new();
let mut software_version = String::new();
let mut commentary = String::new();
let mut state = PreambleState::StartOfLine;
loop {
let next_byte = connection
.read_u8()
.await
.change_context(PreambleReadError::Read)?;
let next_char = char::from(next_byte);
tracing::trace!(?next_char, ?state, "processing next preamble character");
match state {
PreambleState::StartOfLine => match next_char {
'S' => state = PreambleState::InitialS,
_ => {
preamble.push(next_char);
state = PreambleState::Preamble;
}
},
PreambleState::Preamble => match next_char {
'\r' => state = PreambleState::CarriageReturn,
_ => preamble.push(next_char),
},
PreambleState::CarriageReturn => match next_char {
'\n' => state = PreambleState::StartOfLine,
_ => return Err(report!(PreambleReadError::NoLineFeedForCarriage)),
},
PreambleState::InitialS => match next_char {
'S' => state = PreambleState::SecondS,
_ => {
preamble.push('S');
preamble.push(next_char);
state = PreambleState::Preamble;
}
},
PreambleState::SecondS => match next_char {
'H' => state = PreambleState::InitialH,
_ => {
preamble.push_str("SS");
preamble.push(next_char);
state = PreambleState::Preamble;
}
},
PreambleState::InitialH => match next_char {
'-' => state = PreambleState::InitialDash,
_ => {
preamble.push_str("SSH");
preamble.push(next_char);
state = PreambleState::InitialDash;
}
},
PreambleState::InitialDash => match next_char {
'2' => state = PreambleState::Version2,
_ => {
return Err(report!(PreambleReadError::IllegalVersion(String::from(
next_char
))))
}
},
PreambleState::Version2 => match next_char {
'.' => state = PreambleState::VersionDot,
_ => {
return Err(report!(PreambleReadError::IllegalVersion(format!(
"2{}",
next_char
))))
}
},
PreambleState::VersionDot => match next_char {
'0' => state = PreambleState::Version0,
_ => {
return Err(report!(PreambleReadError::IllegalVersion(format!(
"2.{}",
next_char
))))
}
},
PreambleState::Version0 => match next_char {
'-' => state = PreambleState::VersionDash,
_ => return Err(report!(PreambleReadError::NoDashAfterVersion)),
},
PreambleState::VersionDash => {
software_name_bytes.push(next_byte);
state = PreambleState::SoftwareName;
}
PreambleState::SoftwareName => match next_char {
'_' => state = PreambleState::SoftwareVersion,
x if x == '-' || x.is_ascii_whitespace() => {
return Err(report!(PreambleReadError::IllegalSoftwareNameChar))
}
_ => software_name_bytes.push(next_byte),
},
PreambleState::SoftwareVersion => match next_char {
' ' => state = PreambleState::Commentary,
'\r' => state = PreambleState::FinalCarriageReturn,
'_' => state = PreambleState::SoftwareVersion,
x if x == '-' || x.is_ascii_whitespace() => {
return Err(report!(PreambleReadError::IllegalSoftwareNameChar))
.attach_printable_lazy(|| {
format!("saw {:?} / {}", next_char, next_byte)
})?
}
_ => software_version.push(next_char),
},
PreambleState::FinalCarriageReturn => match next_char {
'\n' => break,
_ => return Err(report!(PreambleReadError::MissingFinalNewline)),
},
PreambleState::Commentary => match next_char {
'\r' => state = PreambleState::FinalCarriageReturn,
_ => commentary.push(next_char),
},
}
}
let software_name = String::from_utf8(software_name_bytes)
.change_context(PreambleReadError::InvalidSoftwareName)?;
Ok(Preamble {
preamble,
software_name,
software_version,
commentary,
})
}
// let mut read_buffer = vec![0; 4096];
// let mut pre_message = String::new();
// let protocol_version;
// let software_name;
// let software_version;
// let commentary;
// let mut prefix = String::new();
//
//
// 'outer: loop {
// let read_length = connection
// .read(&mut read_buffer)
// .await
// .change_context(PreambleReadError::InitialRead)?;
// let string_version = std::str::from_utf8(&read_buffer[0..read_length])
// .change_context(PreambleReadError::BannerError)?;
//
// prefix.push_str(string_version);
// let ends_with_newline = prefix.ends_with("\r\n");
//
// let new_prefix = if ends_with_newline {
// // we are cleanly reading up to a \r\n, so our new prefix after
// // this loop is empty
// String::new()
// } else if let Some((correct_bits, leftover)) = prefix.rsplit_once("\r\n") {
// // there's some dangling bits in this read, so we'll cut this string
// // at the final "\r\n" and then remember to use the leftover as the
// // new prefix at the end of this loop
// let result = leftover.to_string();
// prefix = correct_bits.to_string();
// result
// } else {
// // there's no "\r\n", so we don't have a full line yet, so keep reading
// continue;
// };
//
// for line in prefix.lines() {
// if line.starts_with("SSH") {
// let (_, interesting_bits) = line
// .split_once('-')
// .ok_or_else(||
// report!(PreambleReadError::InvalidHeaderLine {
// reason: "could not find dash after SSH",
// line: line.to_string(),
// }))?;
//
// let (protoversion, other_bits) = interesting_bits
// .split_once('-')
// .ok_or_else(|| report!(PreambleReadError::InvalidHeaderLine {
// reason: "could not find dash after protocol version",
// line: line.to_string(),
// }))?;
//
// let (softwarever, comment) = match other_bits.split_once(' ') {
// Some((s, c)) => (s, c),
// None => (other_bits, ""),
// };
//
// let (software_name_str, software_version_str) = softwarever
// .split_once('_')
// .ok_or_else(|| report!(PreambleReadError::InvalidHeaderLine {
// reason: "could not find underscore between software name and version",
// line: line.to_string(),
// }))?;
//
// software_name = software_name_str.to_string();
// software_version = software_version_str.to_string();
// protocol_version = protoversion.to_string();
// commentary = comment.to_string();
// break 'outer;
// } else {
// pre_message.push_str(line);
// pre_message.push('\n');
// }
// }
//
// prefix = new_prefix;
// }
//
// tracing::info!(
// ?protocol_version,
// ?software_version,
// ?commentary,
// "Got server information"
// );
//
// Ok(Preamble {
// protocol_version,
// software_name,
// software_version,
// commentary,
// })
// }
/// Write a preamble to the given network socket.
pub async fn write<W: AsyncWriteExt + Unpin>(
&self,
connection: &mut W,
) -> error_stack::Result<(), PreambleWriteError> {
let comment = if self.commentary.is_empty() {
self.commentary.clone()
} else {
format!(" {}", self.commentary)
};
let output = format!(
"SSH-2.0-{}_{}{}\r\n",
self.software_name, self.software_version, comment
);
connection
.write_all(output.as_bytes())
.await
.change_context(PreambleWriteError::Write)
}
}
proptest::proptest! {
#[test]
fn preamble_roundtrips(preamble in Preamble::arbitrary()) {
let read_version = tokio::runtime::Runtime::new().unwrap().block_on(async {
let (mut writer, mut reader) = tokio::io::duplex(4096);
preamble.write(&mut writer).await.unwrap();
Preamble::read(&mut reader).await.unwrap()
});
assert_eq!(preamble, read_version);
}
#[test]
fn preamble_read_is_thrifty(preamble in Preamble::arbitrary(), b in u8::arbitrary()) {
let (read_version, next) = tokio::runtime::Runtime::new().unwrap().block_on(async {
let (mut writer, mut reader) = tokio::io::duplex(4096);
preamble.write(&mut writer).await.unwrap();
writer.write_u8(b).await.unwrap();
writer.flush().await.unwrap();
drop(writer);
let preamble = Preamble::read(&mut reader).await.unwrap();
let next = reader.read_u8().await.unwrap();
(preamble, next)
});
assert_eq!(preamble, read_version);
assert_eq!(b, next);
}
}

113
tests/all_keys.toml Normal file
View File

@@ -0,0 +1,113 @@
[runtime]
worker_threads = 4
blocking_threads = 8
[logging]
level = "DEBUG"
include_filename = true
include_lineno = true
include_thread_ids = true
include_thread_names = true
mode = "Compact"
target = "stdout"
[keys]
[keys.ecdsa_clear1]
public = "tests/ssh_keys/ecdsa1.pub"
private = "tests/ssh_keys/ecdsa1"
[keys.ecdsa_clear2]
public = "tests/ssh_keys/ecdsa2.pub"
private = "tests/ssh_keys/ecdsa2"
[keys.ecdsa_big1]
public = "tests/ssh_keys/ecdsa384a.pub"
private = "tests/ssh_keys/ecdsa384a"
[keys.ecdsa_big2]
public = "tests/ssh_keys/ecdsa384b.pub"
private = "tests/ssh_keys/ecdsa384b"
[keys.ecdsa_biggest1]
public = "tests/ssh_keys/ecdsa384a.pub"
private = "tests/ssh_keys/ecdsa384a"
[keys.ecdsa_biggest2]
public = "tests/ssh_keys/ecdsa521b.pub"
private = "tests/ssh_keys/ecdsa521b"
[keys.ed25519_clear1]
public = "tests/ssh_keys/ed25519a.pub"
private = "tests/ssh_keys/ed25519a"
[keys.ed25519_clear2]
public = "tests/ssh_keys/ed25519b.pub"
private = "tests/ssh_keys/ed25519b"
[keys.ed25519_pass_here]
public = "tests/ssh_keys/ed25519a.pub"
private = "tests/ssh_keys/ed25519a"
password = "hush"
[keys.ed25519_pass_on_load]
public = "tests/ssh_keys/ed25519b.pub"
private = "tests/ssh_keys/ed25519b"
[keys.rsa_reasonable1]
public = "tests/ssh_keys/rsa4096a.pub"
private = "tests/ssh_keys/rsa4096a"
[keys.rsa_reasonable2]
public = "tests/ssh_keys/rsa4096b.pub"
private = "tests/ssh_keys/rsa4096b"
[keys.rsa_big1]
public = "tests/ssh_keys/rsa7680a.pub"
private = "tests/ssh_keys/rsa7680a"
[keys.rsa_big2]
public = "tests/ssh_keys/rsa7680b.pub"
private = "tests/ssh_keys/rsa7680b"
[keys.rsa_extra1]
public = "tests/ssh_keys/rsa8192a.pub"
private = "tests/ssh_keys/rsa8192a"
[keys.rsa_extra2]
public = "tests/ssh_keys/rsa8192b.pub"
private = "tests/ssh_keys/rsa8192b"
[keys.rsa_crazy1]
public = "tests/ssh_keys/rsa15360a.pub"
private = "tests/ssh_keys/rsa15360a"
[keys.rsa_crazy2]
public = "tests/ssh_keys/rsa15360b.pub"
private = "tests/ssh_keys/rsa15360b"
[defaults]
key_exchange_algorithms = [ "curve25519-sha256" ]
server_host_algorithms = [ "ed25519" ]
encryption_algorithms = [ "aes256-ctr", "aes256-gcm" ]
mac_algorithms = [ "hmac-sha256" ]
compression_algorithms = [ "zlib" ]
predict = "curve25519-sha256"
[servers]
[servers.sea]
host = "sea.uhsure.com"
encryption_algorithms = ["aes256-gcm"]
compression_algorithms = []
[servers.origin]
host = "104.238.156.29"
encryption_algorithms = ["aes256-ctr"]
compression_algorithms = []
[servers.origin6]
host = "2001:19f0:8001:1e9b:5400:04ff:fe7e:055d"
encryption_algorithms = ["aes256-ctr"]
compression_algorithms = []

View File

@@ -0,0 +1,53 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1,9 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNl
Y2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTXRwCTNcWIwWe1HV/x
m4Id6Dch54nlfBRq1/5DROdn2AVm4ZYSj8kjHI8lDqfxzXZ66reHJAcecmNrgd4g
geeCAAAAsMnOb8HJzm/BAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy
NTYAAABBBE/n2mCAHZDC0V1sX473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri9
3RQaw1U+yQImCKvhv4uEDn0tf2BuOHIAAAAhAIJyO57UJPPaZ4EWPefrZ9/zVe6j
oI4ioVoVubbq38D9AAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQYH
-----END OPENSSH PRIVATE KEY-----

9
tests/ssh_keys/ecdsa1 Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTXRwCTNcWIwWe1HV/xm4Id6Dch54nl
fBRq1/5DROdn2AVm4ZYSj8kjHI8lDqfxzXZ66reHJAcecmNrgd4ggeeCAAAAsMnOb8HJzm
/BAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdHAJM1xYjBZ7Ud
X/Gbgh3oNyHnieV8FGrX/kNE52fYBWbhlhKPySMcjyUOp/HNdnrqt4ckBx5yY2uB3iCB54
IAAAAhANE9lmc43afpjYdta5hvNo6Hgms6QC2ZiPMmdvfhP/8mAAAAEGFkYW13aWNrQGVy
Z2F0ZXMBAgMEBQYH
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdHAJM1xYjBZ7UdX/Gbgh3oNyHnieV8FGrX/kNE52fYBWbhlhKPySMcjyUOp/HNdnrqt4ckBx5yY2uB3iCB54I= adamwick@ergates

9
tests/ssh_keys/ecdsa2 Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRP59pggB2QwtFdbF+O9/2KuYZLFDjP
MKDQKc3KuHwPT3xL2ybcUd64vd0UGsNVPskCJgir4b+LhA59LX9gbjhyAAAAsAptbc0KbW
3NAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE/n2mCAHZDC0V1s
X473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri93RQaw1U+yQImCKvhv4uEDn0tf2BuOH
IAAAAhAIJyO57UJPPaZ4EWPefrZ9/zVe6joI4ioVoVubbq38D9AAAAEGFkYW13aWNrQGVy
Z2F0ZXMBAgMEBQYH
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE/n2mCAHZDC0V1sX473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri93RQaw1U+yQImCKvhv4uEDn0tf2BuOHI= adamwick@ergates

10
tests/ssh_keys/ecdsa384a Normal file
View File

@@ -0,0 +1,10 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQQNo4tCmBW6DJv2Xshowx5Esm8SL8iX
gSNgoMOv8T9f1upJnNZ0NNBq0DTk2EIAlGFjFRGwY1LMvuXCEjPcNgNgbLkRpuSBZEpUdC
v7vP+Q4CwAwa6ZLg/bUOxM6L3xD9wAAADY90b9x/dG/ccAAAATZWNkc2Etc2hhMi1uaXN0
cDM4NAAAAAhuaXN0cDM4NAAAAGEEDaOLQpgVugyb9l7IaMMeRLJvEi/Il4EjYKDDr/E/X9
bqSZzWdDTQatA05NhCAJRhYxURsGNSzL7lwhIz3DYDYGy5EabkgWRKVHQr+7z/kOAsAMGu
mS4P21DsTOi98Q/cAAAAMFXAE7Bhbcr4CW9xJhaXm4ToN6lVq3OKw4C41ZxxIetLxfYhyZ
IMpkNIlxhsT5JCugAAABBhZGFtd2lja0BlcmdhdGVz
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBA2ji0KYFboMm/ZeyGjDHkSybxIvyJeBI2Cgw6/xP1/W6kmc1nQ00GrQNOTYQgCUYWMVEbBjUsy+5cISM9w2A2BsuRGm5IFkSlR0K/u8/5DgLADBrpkuD9tQ7EzovfEP3A== adamwick@ergates

10
tests/ssh_keys/ecdsa384b Normal file
View File

@@ -0,0 +1,10 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSFuBPbGxyIn0K7nT7gLt1mO+RP7TuS
zFVatSmKS7UDl8EaBvEHSCqZWWvpaz1hw2l7fENakdh2aAmG4ZQiQU2k9LLzOJUmQvkJzi
x0d35GiyHvrgCbk+3jux0nRFqjf1EAAADg33k9/t95Pf4AAAATZWNkc2Etc2hhMi1uaXN0
cDM4NAAAAAhuaXN0cDM4NAAAAGEEhbgT2xsciJ9Cu50+4C7dZjvkT+07ksxVWrUpiku1A5
fBGgbxB0gqmVlr6Ws9YcNpe3xDWpHYdmgJhuGUIkFNpPSy8ziVJkL5Cc4sdHd+Rosh764A
m5Pt47sdJ0Rao39RAAAAMQC7FlZiz5mGgDtp1i9/Wy/gpWySe5xiER/COiIPFMm3wUchO2
tbJ06GM6maApazHuMAAAAQYWRhbXdpY2tAZXJnYXRlcwECAwQFBgc=
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBIW4E9sbHIifQrudPuAu3WY75E/tO5LMVVq1KYpLtQOXwRoG8QdIKplZa+lrPWHDaXt8Q1qR2HZoCYbhlCJBTaT0svM4lSZC+QnOLHR3fkaLIe+uAJuT7eO7HSdEWqN/UQ== adamwick@ergates

12
tests/ssh_keys/ecdsa521a Normal file
View File

@@ -0,0 +1,12 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBaJo3rKMWJqSqcuYkQExpvxGezbZj
khCmt26YtKCBxTyI+ptgV0wPujZOxuA+pYY909WGlulKHAzicP8feQZdUpoBK4eOwunitw
EAphr3I0dqcBAJWd4KIMov11qYvcNb8nsUbmulk10+IVxJQL+bwHhLsenHArFRlBrPiHv+
6thVibMAAAEQwd7yr8He8q8AAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
AAAIUEAWiaN6yjFiakqnLmJEBMab8Rns22Y5IQprdumLSggcU8iPqbYFdMD7o2TsbgPqWG
PdPVhpbpShwM4nD/H3kGXVKaASuHjsLp4rcBAKYa9yNHanAQCVneCiDKL9damL3DW/J7FG
5rpZNdPiFcSUC/m8B4S7HpxwKxUZQaz4h7/urYVYmzAAAAQgEt3nVbegR+JPwhB+zxq7Ah
1iKnuXdyNJytMm9PzOtC/0ufeCFtvWHB4J20ysAisdHfreftxJBh53yrSqmFYNmtkgAAAB
BhZGFtd2lja0BlcmdhdGVzAQI=
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFomjesoxYmpKpy5iRATGm/EZ7NtmOSEKa3bpi0oIHFPIj6m2BXTA+6Nk7G4D6lhj3T1YaW6UocDOJw/x95Bl1SmgErh47C6eK3AQCmGvcjR2pwEAlZ3gogyi/XWpi9w1vyexRua6WTXT4hXElAv5vAeEux6ccCsVGUGs+Ie/7q2FWJsw== adamwick@ergates

12
tests/ssh_keys/ecdsa521b Normal file
View File

@@ -0,0 +1,12 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBYa7n+gYpTaP+AqX+rhHoYLbzSFjB
namxsbIoqAvhcqFXpXqDTyfP5bbe+/xr6iiJZOuAxrxt30SD6r5JR4ls48gAKHuqEOsphS
vKo4BucAQoJLfdD8Xl22RCRN9lpTzhvbl9iYUn0YV1scAU8xMmovBTdn9Z6QrIYqG5CMNn
2fIr4TkAAAEQo4W2lqOFtpYAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
AAAIUEAWGu5/oGKU2j/gKl/q4R6GC280hYwZ2psbGyKKgL4XKhV6V6g08nz+W23vv8a+oo
iWTrgMa8bd9Eg+q+SUeJbOPIACh7qhDrKYUryqOAbnAEKCS33Q/F5dtkQkTfZaU84b25fY
mFJ9GFdbHAFPMTJqLwU3Z/WekKyGKhuQjDZ9nyK+E5AAAAQgCPHZ4xrsWwGgVy8H54DRD0
g8ihcJ7Fsa7I84mTd0N4x2EbHvrmDChyreZ9MelKG8Jvwea0D4BtnCDPXEfkHAw4NwAAAB
BhZGFtd2lja0BlcmdhdGVzAQI=
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFhruf6BilNo/4Cpf6uEehgtvNIWMGdqbGxsiioC+FyoVeleoNPJ8/ltt77/GvqKIlk64DGvG3fRIPqvklHiWzjyAAoe6oQ6ymFK8qjgG5wBCgkt90PxeXbZEJE32WlPOG9uX2JhSfRhXWxwBTzEyai8FN2f1npCshiobkIw2fZ8ivhOQ== adamwick@ergates

View File

@@ -0,0 +1,8 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBUz8HBJf
hkFUWB/uRKkshkAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIN8pW2btKRHc65DS
yKjL7XBtvvPTHGd5KoI6L4KatxRZAAAAoDCMQNOT746RIs2IR7DixViQ4QjzodwVo//Y2Q
J/aE/PdKJ34kmngtnyyredzNseTG838n5PpW2Jo5R0JpGcRcyX/KC0lUHfDlnyHo29sNqx
fI39BEZnV87bA+pJhHKhLGxplWY9Hns+cpsh3mgNZGOC3M9zT1geO3AHdXMhI4HnYxw2/G
OUW1muIrFzKaRV2bl7kKwH1DRyYqp3VxfS+oo=
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8pW2btKRHc65DSyKjL7XBtvvPTHGd5KoI6L4KatxRZ adamwick@ergates

View File

@@ -0,0 +1,8 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCuOPKhvD
nx/dHJW4bC7y4uAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIET2SpCeHNG/3IIL
j80xTImmn2JEcTRVM4khSQ/rCQ3FAAAAoIoSFrEKoEIGSAsr07HI/tSt2W/DKgeTYsUmNi
r8suuyfNqETjlfZ9FWxw0UWW1mxA/XzLvRYx+kGPkqKM8DsCpVCMl2S/3hMCCoxhE5mKP9
PE+VV0DKa6jywEcgMEiMejoRNnfHAAvI9KHAPPdIR11geqEMAx3nZFRy0IyZwxNYPFbijM
mjv8RcoFGVoHT2R4abpO6oEADjJ0KqZRNdg9k=
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIET2SpCeHNG/3IILj80xTImmn2JEcTRVM4khSQ/rCQ3F adamwick@ergates

7
tests/ssh_keys/ed25519a Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCA4kdqb8sTeg7amwY8Tdck2zEbqcXDRFva/4VRFWNY0wAAAJj4Nb/Z+DW/
2QAAAAtzc2gtZWQyNTUxOQAAACCA4kdqb8sTeg7amwY8Tdck2zEbqcXDRFva/4VRFWNY0w
AAAEB+W/KcnOrffyr18T1GttW8Z6yutReqViIkm6cgOUAA7YDiR2pvyxN6DtqbBjxN1yTb
MRupxcNEW9r/hVEVY1jTAAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDiR2pvyxN6DtqbBjxN1yTbMRupxcNEW9r/hVEVY1jT adamwick@ergates

7
tests/ssh_keys/ed25519b Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCfk3xXxqLhgBYvgUyAC+vBu+3XBD/CY8SY7wp2hrXhEQAAAJj50iU4+dIl
OAAAAAtzc2gtZWQyNTUxOQAAACCfk3xXxqLhgBYvgUyAC+vBu+3XBD/CY8SY7wp2hrXhEQ
AAAED8AA2vVnMt9sKP6psihSU9ldHjV9yv1pOA2XmdyDcCsZ+TfFfGouGAFi+BTIAL68G7
7dcEP8JjxJjvCnaGteERAAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ+TfFfGouGAFi+BTIAL68G77dcEP8JjxJjvCnaGteER adamwick@ergates

170
tests/ssh_keys/rsa15360a Normal file
View File

@@ -0,0 +1,170 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
13aWNrQGVyZ2F0ZXMBAg==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-rsa 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 adamwick@ergates

170
tests/ssh_keys/rsa15360b Normal file
View File

@@ -0,0 +1,170 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
dpY2tAZXJnYXRlcwECAw==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-rsa 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 adamwick@ergates

49
tests/ssh_keys/rsa4096a Normal file
View File

@@ -0,0 +1,49 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC3ftJT8JKsBlMHj+mJ2JLUCH7da6Rn2KxKO4+9LzoZRnylf8EB7uO/nq/tyLyMMHBv8d25m0xne498z4VvKF/r4wt/gvWkmcauHL3WqTTJBfzc4oSGacVrCvUXX1LaSt/ZSuIUDLqWBE4lONFmdfInaB8ozqWjIgX3njhw9yNl/k53ZnAWiaOnG71tlIWha+jxubZ/T7RTp/4tiWpubWOF4QCDAbAAijDe3C8wvIlmKihZMdl0nPLx11Ope+uX/VMTZlmdYUpy9KkiyKwO2A5PFVmbqpb51WHVBEwJDVpOOda+Fow24R1ZWqVcUGtvau1jBLxC7MvqNPx1zNHKRWbQyRSug9B8DgYdTxVkU1bb8kmDA67apyl8Qr+CB7xECRUyTcDfigSJ2djbBaVgIe3LVHQeJAZNaZNy6FnhkxpuSBYxOBAOCzTrIIacYGivMF5/JjfO2cFH3y26UJbP+PXoZSYYSojg2Ksk3j+pZJTjr3tDqnJkfO/bMId9cNe+Csp55rg+IzcXfQxBPdmS1oyVi2MLY0mYDx/BlZRvIg5Hj+4SuY4owpSi1Ap5aZOKb/Su0YURu2zVQQBxmyTkbQoFB0wopoiM6nQZZCZayCjM36jqp9rsA83L89drtkrYUESR3rJ2boUhSy9lV3bT2frSmMtHqjNpToN1/o6sCva2tw== adamwick@ergates

49
tests/ssh_keys/rsa4096b Normal file
View File

@@ -0,0 +1,49 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAgEAs09DRmrrXl/Ykvy0Lt4Lp5QzOBvLAgeZhtHYlXpZgqI+kKXhl0nO
iyB/QEEneVxUOqf587FOTXl46Y7DpKL+wA5ejC7KcdEHL0+PSsKQJOZR1OyVQBKvQcEQu+
DB2yl54N0XNfGxMtUVSRwrq8SxGzmJTWxjkxrBWnPTPdEj2QIP7PkYOtxyb64Oh0yNCI8o
21Q7ghKxCxnaY9sXNqa1s7nRprjJEqpV94W7+ijq1LlBaG8EZ8kaheLFkkNjH+bLY4ugyM
2ki1gQeRmizGA3uMVgQuD9uv380Rd883+7fVpUeBvtmBUfbsx8GZnUabFSm1leHqRTVTry
ebhREMnsPvgHb7OA+NahQ4zlB5IR+171kfnHajMZMcN1YPmhdfpWBWIjMnmlMV3mKJEu8J
hy9NRf4Blh65UzcMwvKkQQZ4rDYobg2J9GmaMS+IGiSmPvW2YYpb25Nvl6eQIJBN+i/pIz
xWWCrBAoRsuqkivzIKGvh0IIgg0Aor11VQhpwF+IVlucM5IHQjIjld3Edg1yJQF4Cs7Kw+
DmPmdsGihMNWbDIrgNt7wHYz3XSpj9E12w4y0xAIDu4l3lfkSKttk9+JkvjBlux7Fx+In8
zsRU4uDOXmcmPOdxwxX9ElIo1Tc7mX0JAw460TVJ8BoWpoioH7t40TvZlqy1HsLSW8P0Vu
0AAAdI5SXQkOUl0JAAAAAHc3NoLXJzYQAAAgEAs09DRmrrXl/Ykvy0Lt4Lp5QzOBvLAgeZ
htHYlXpZgqI+kKXhl0nOiyB/QEEneVxUOqf587FOTXl46Y7DpKL+wA5ejC7KcdEHL0+PSs
KQJOZR1OyVQBKvQcEQu+DB2yl54N0XNfGxMtUVSRwrq8SxGzmJTWxjkxrBWnPTPdEj2QIP
7PkYOtxyb64Oh0yNCI8o21Q7ghKxCxnaY9sXNqa1s7nRprjJEqpV94W7+ijq1LlBaG8EZ8
kaheLFkkNjH+bLY4ugyM2ki1gQeRmizGA3uMVgQuD9uv380Rd883+7fVpUeBvtmBUfbsx8
GZnUabFSm1leHqRTVTryebhREMnsPvgHb7OA+NahQ4zlB5IR+171kfnHajMZMcN1YPmhdf
pWBWIjMnmlMV3mKJEu8Jhy9NRf4Blh65UzcMwvKkQQZ4rDYobg2J9GmaMS+IGiSmPvW2YY
pb25Nvl6eQIJBN+i/pIzxWWCrBAoRsuqkivzIKGvh0IIgg0Aor11VQhpwF+IVlucM5IHQj
Ijld3Edg1yJQF4Cs7Kw+DmPmdsGihMNWbDIrgNt7wHYz3XSpj9E12w4y0xAIDu4l3lfkSK
ttk9+JkvjBlux7Fx+In8zsRU4uDOXmcmPOdxwxX9ElIo1Tc7mX0JAw460TVJ8BoWpoioH7
t40TvZlqy1HsLSW8P0Vu0AAAADAQABAAACAEoh0AeR9sNqzuheL8Rcquban55n5zNsnu2d
XnTWQ6F9oG4/FphsvEbK5bFT/pTvNieWAQHeYSgou3OcQYiUlswiZLaCNdJ+gADwXKak7+
FBk717Hm2CDBEcV+XFE4CfkjMEVS9JQGBqtkUmr2txg2NlEz3+POC5pAzYbBJXoAF9F8Z6
aakUMP+5L2qCnKBYR6T+Gyg4wBd91cuI7fz7SY4HmgTays67u5T9Jm1Tc1sFSGR72Y9rFl
saGWLSF24+BgKe3JeIZanye8UFc0gZ04/Bkn2z9VLU5SwxEMi/G23E5b1OlplUyk0Nn5Ua
Aza7SBLQDNiQSZ+oIk1uhZ1yTgg9WBAryc1FAQutv2MtXRKNK8P7aQm96AMckhESZ5d0BH
YkhAlz4UthAc3D7sTpt0od5ufW0g0cvTKVust2Fn6LZ3yr2FnDZ3R3D1/O40NMZ61yZv7S
Nr5VN3UqKIv40zgG8qpxijMXG7lfmflNWGojCOivj68vH50zLJ9nSRuuHN+zIQyy5x6uSk
2+fC9efMkoTC5Y6z7VON/6y7jL3hmXR9pMVTiSf9HIqqgCKMvvw2nhf4Nag8lRuOENJPd0
1/dXcUOIRW8hzX9+yujC3azl48RAULw161S/zkxoy8SA8KBVnn9LBAfE502ZcfaENtJ5pj
bMjFbVbl0PKyvS+bIlAAABAQCMcJ9seKFPbALpSuRnnNoKjogZNwV3Nel9pu8MxuppWHK+
h10p8VQKKPyyzuKt3rv+N4XQaykZYuAdpECPCseb6Dk7S5icGyDF1juuQsL+7jDSBqt2bz
0AeiwAzm4Bbev1N9XxDr5/JR51MPGWU53t0jaKp6X7vniRvNDId3LIvGdnJPQpr2iYTyL6
+9dLdFtO95CYCeQ5S/NxFXLcm22F3eDWlv/ygmwgMzMJloiQ2Lbvfvdq9l/hXLMLH54gJn
1dwyQxSFIeCu/oA+21y8ddq6JjlIG4TcfySYSg+ODreuP6uO6H3PdGuTxbx6PiOR4MFR5e
+xp+eFgj3Ah/Ipj1AAABAQDczZJ/R7E9GOSxJKXhDQX+nYviYZ5Nxy5B9hSSo3go8UmA14
EflGypqxRkvcQKZgeZZPGpZ96pipaGP8NX1rZz0yTryjIGm6Zg1VidVUET/kDoGD6nwI9m
IBL2LXvkWCUMkZ8LHycRRSrjzAPBHuqDD3wNGLmkrzVoMviphrTtMOiRlrnN5KuZ++Nsh4
iGsOHVkJ3x7rzoExJxbQ7vwfIZ8URMfvBfrKO21rYw2zoH7C58NplCmQ/mf47RiOl/iLVT
iVStrhuPnAlY06RrrbUr66XpSOID8QO+O/tQgwMyR0lPT0kd+WzwbDRBgEEdNv/lF1+oT8
wNk+JN1vbui0IHAAABAQDP5HJ3+jF5StZQfkhOcYxDLJRQZg9AtyqWc6bzJBtlBeQXi7be
0M0l3O3kTycDfXwwmxdGeMCBU4DubAgtS7jzlTTHLHradCurWCzlEzOzQKF1or187GUizs
TagxuYiaAL+xUW1lQbiTRYYpiqZj0iqyhUzVPYra027psu836SQwqVJsNvPCZRK6ynDkrv
TFgDdlyN228w6ZJThIq1thkviRfRAgxTephx4ZyIs79sNGMpztRVJFfhUgyH2+/4dno88R
Wl2aPo84rUge+SKQv7AREsCwXZLincZcodpCUtEgGkfc5yHk/5jbY+4NRqeZfJrPpjmLm9
YZfJvW6yOtJrAAAAEGFkYW13aWNrQGVyZ2F0ZXMBAg==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCzT0NGauteX9iS/LQu3gunlDM4G8sCB5mG0diVelmCoj6QpeGXSc6LIH9AQSd5XFQ6p/nzsU5NeXjpjsOkov7ADl6MLspx0QcvT49KwpAk5lHU7JVAEq9BwRC74MHbKXng3Rc18bEy1RVJHCurxLEbOYlNbGOTGsFac9M90SPZAg/s+Rg63HJvrg6HTI0IjyjbVDuCErELGdpj2xc2prWzudGmuMkSqlX3hbv6KOrUuUFobwRnyRqF4sWSQ2Mf5stji6DIzaSLWBB5GaLMYDe4xWBC4P26/fzRF3zzf7t9WlR4G+2YFR9uzHwZmdRpsVKbWV4epFNVOvJ5uFEQyew++Advs4D41qFDjOUHkhH7XvWR+cdqMxkxw3Vg+aF1+lYFYiMyeaUxXeYokS7wmHL01F/gGWHrlTNwzC8qRBBnisNihuDYn0aZoxL4gaJKY+9bZhilvbk2+Xp5AgkE36L+kjPFZYKsEChGy6qSK/Mgoa+HQgiCDQCivXVVCGnAX4hWW5wzkgdCMiOV3cR2DXIlAXgKzsrD4OY+Z2waKEw1ZsMiuA23vAdjPddKmP0TXbDjLTEAgO7iXeV+RIq22T34mS+MGW7HsXH4ifzOxFTi4M5eZyY853HDFf0SUijVNzuZfQkDDjrRNUnwGhamiKgfu3jRO9mWrLUewtJbw/RW7Q== adamwick@ergates

88
tests/ssh_keys/rsa7680a Normal file
View File

@@ -0,0 +1,88 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAADwQDawzhmtS+TPz4sxdIpD+UhMDXam7dTg3zbRO8WCzVYSIly5T7tJfV3EBtzfRqzjsV/KhyY6c2tA0sF8xSbcX/az6gWcYSO4BDeKKFjXq7VCKiK6eSZT0KE2VY3sdVdP/uRRvuXsZlRNz98Wpv6nR2oX+8N+rf+X8MAouNPeh2MAcPsAPsZs6aaOkVSRWpZCGaB7OJDY6r2WnS68K8KkJA3W5F5BPr4yHSpzbqWaxveYyrx3esNFoDrUZdFhFVmsuLO4B+S7l/cCXVfEoNKLRbUMySmydXaMSzE85LHqzGlvICLhf4RylMy1WmYWuaZSoIjgu3Aey4KAnU9vtFv7GMB+Imv9nTRDUN5U5wm0Jm8w0j8b9apUo1lQ241CS9DfjLKBgBwwNv7/C+m3e9q5erwcmVNNiutZwFa07FEZBfzL9HQdnLk8AJNpn1Srt75+jzsUOFtDSENaBbjozZookIe1yOm5erM44Y0GaEYMmDuhvqvvB5xOyKBqAsFhN9KmOP2q1qttMbGBe12CuIglB8Rqjeja/T/n02VUSniKLZ1XaclT5g2dwNZtSZluNdip5J3ja/kYCt+pUenYSiD+FEljgofAEi0vznnScjjPXa2Q98N4jw9q3W7+btmOAEdoyJxz8syiE8FNvFHgOCMBm75oaihzgzZkzgpcDMdBjIjCHT4PM3Umengu1dg3YFoOLcU7j+oxuSajVbLibodPfgJcQl5XYVWVwOYM5/YddywxrS6hjS0tc+JGz4oSZGN8SPSw1inkgJ5mQSyOpuYSeJnZDhLgGt8B38fOs7s9hI44MSefwfycrVWt3KTwy+UJdhGiRKqnpzdpvSNXdkQpwFDbUwZJi6hJv8zPj2aNZenm1IjQ8EJN/NS0p2xYUIuBi99aecO9Xf2sIYB98K6KZCMbB1XogUE0gdKeAhkU19ZJ4/ZaWsLiVh97u2mhcpyDCZCY0HXkpvVeLkJ1h2wgomd6pWX1fhzBBGknTCWT8B5QZ0d8vwo1jqrzMaZgpabU08aSgO/OVHd0Yytz45AX8QYrg9EpFPgBxlnF1+HE1WrT70nbkyx06OFNw6VqpW7tLBYVq1F9ay7JTObakdwfDJwu82Dy347XLBVVU4VKC2pc50fJdmEO5X4dS2n3FT99sJqYD1tbRjAXorQIjJYcLOHmHIIakZQxEtOf5SpSaStFD/pH4BGb/wrKgmtzKMD4Ya7BkiIS5f6popvAaPmKlCYKJHrFQrYFxMC6p7z5WXfTIPr8sCTsLLWAy1J33Zk1r8= adamwick@ergates

88
tests/ssh_keys/rsa7680b Normal file
View File

@@ -0,0 +1,88 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAADwQDrtJEKsOQsAUx9qmDa82CA0HswihkxCOpFJWUXFvWlxuqrCxNck7s3ceyhhxDlRpugq3DKPxQgFUnBnFhgNmqmeKFQdI4fjn5EhEptf7tu0+aF4rnUj4DnD4uzbd/b5J8BnMQ8iTzdXuxkILUeugZiyYjJz9dnJY7uQbAfjz3EHSLVcxryCd4lqnbLE8b6cXuZkUKva4RhRNUajyfoProkiWD6jYIebeMt/ZCMddatmGtH1+dVmxPs4Q5DiTDcS8w+1YKCcEYaKkMkzGg6QgIbs1Do24qJGq+E8hLX38G+5sJGZsCyy/tIxnzOIZuRDvrpJMZmiaBgtjtUriJmod3gJPFL9DRztetUZUI7AV6jDrvppiXyf+LAphUzB/Z0mvsM/pSk780DCqdzlYsesgFRlCPBy2ZXxQxrHEWzwlAmqZsMYJ7G9KPu2OqFclp7qIPgoftx4RnKSH/fEaaixtoYhPVQVZWWg9n5Yhxps7NLGKYpBmCD7iDRfvkSxKSVrIPuMmz16fAe3F0AAxTwnoCnQ4ILW/E/wQY3wp03OzonY/r+G6u4x1OpxcIgdPA2p/qbTWhNa/NRz2SxlB13DI9YLTTMKnx7GVzLUcaAnimQUChEQKuAdsobkjB2Xp6PVDSt9VtuQZ/xCebNqZPRsm+gqK9sJFmXXJqAXGc+e+q7XTtrukqMvi+B2NGIeP3H1FAqZkrfUwOjq4BsSrESPrRXqAUq2ElY21hsh4+vOio4B7tnNzc3BzvExvuZoFbIn4khfJMCNZqPu/FsCYzKOk0wV3yeNu5qBjNmEs+9CohuN9RUsV3jeGgebw8gdJu0dOcsrWst3DhyiqfFZyJNI9moKTterSTpb7vW6juP7cdMoaUPm4a9j+LNbpYEjOtEetofr32L3xiCVcVIDTz7MdhWER2On8JS88M/vUKS7F5LhiDhmpZGNhSVlnj5Fr+EoDQlckH9pm7ytqrKHTG5rg8cVfnVbaJjiFQbXzvGcepHv+LrySyHTP1JyrgZdigRd6dK5V82hXebxhNSbmoLIQ9U79gn0GGs7rGGuldusX4uisAUMfWB2JKdk7/uFwxoKD8wx6EumGUTpI2wsTblnEh2jYJjDiSSLq1DydHzXfESK/upXA8UawUPOfIDcTOQucdeyMLhIsg5HZMR2tn/SxaLEoza+HzvrevgEQhGU0VC84HVb7eAXYIDoLf8yOabhi+2I3UCinQGA8PcwEYhvJ7opOU4HzTg/WMMI9+e3s12cGeKy+HzYRSBEyqXrwSFGIc= adamwick@ergates

93
tests/ssh_keys/rsa8192a Normal file
View File

@@ -0,0 +1,93 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
VtmGIwAAABBhZGFtd2lja0BlcmdhdGVzAQ==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAEAQDC+RrYSdcY7hMA8xtWk3KRdV2BhjqCMkbOpkGA6ri2mtkeGgQEgrUtK2pVSpkU5e0hsQhwiiapgXLDhtw7vYrKY600SXfDDqWFapBcEzD+/S82BlYY3g3LybD+T4m0zrPIhFsvkt0yVCRKEz8zO/tMc20TZsJp/72oRl0fNnTe9KCgNpn9iiMtH6vDRKI4rGZju9nVqCNEhRhugALpfzFFdLtq0JuKwBWBik3xZvkzMNauvHuj3uF/FsS9VQQi8sP9J81lB7WtyTertQrKpTe+qEJf91Te2zxl5PZ0yoV10l09M0khue2O3xrZi+o/IuCaeCgs9dDkQ5KclDxdiRZv2uijzdUrTPHQZBF5cD3EnVZ6VG+v0S49AG07NVd5KJe/OdYpocfcrirdrYJ9q/6W2loDRBYswCwHSl7SPGFBvA8e2TZ8CRFtso1veU+AT+pNUwvmiqv8YO/JAjTCfcEXZW2swcJMwiebOyEZGWixTlepxwNqsobhs7+osKvhgaTTj/BKPSW9LrViy2/V2umunyzgDd/Ih3a8Ll7U5k85Cl+cWqhPIgVvQqDxUgUfxLgHFeGu4Y8wPZfsDw4LosyBaGlLfNAzsrjhIiCeN1l4+6WAoYMtIuINmpm/4W3cwo5SiaO8C7CMiZA4cYTki4LFaYbiCo1oghqdPhMOIPCBol+fuGAeuEP+W+YD18BwzS+/GE77XFSKbF+3N4XfPxWRyBNOEe0Ul2XA8rytr+AwmkDVUV8AQ/WnOpleMVW1Uz/l/DzArEKRYEUh30l71wFJP14ngPppggpEnhysogtcb3mbPYDiUGGX9/a/YRU2qvWshJRLdr6b3AxhrCDw8AoTuorTTaor1fjcmdcKn9d2ZEnxkN4yc1368EKS728WoF/qj7GwJhzs3w7/CkJ7WyrVeG0vkRfcZVJl7LBgAoeRPTyZAY7AgL5nf4qpCTsEXuTk/pgGaUknhu8heVzL6gUtRXQvlJfxZgOdusuGMQ6ZPbfYeV4+Lwpw0lLAXcH0O/tMQoGnB097zDTLzLt3Swdfdo7e6Ef0HnzDGCGRF/hFmr6VAGbpu7nhGX6ySSNx3gpSj1NRMCqWzml/uPSnNK4Wun/tycscWaxB1GEgvl+G4bNzrdFW+GZenbVCvkCyQua2u0JQERGr1JZ25nGvCIDIl7i6EW1dvFUpJJ+OZ87twYGH5xZc3oOII/YSzlUci3E0LJUFodfSUoO96FoL/c+2uvz2jGHlWdqDN5klywX1bjjzD/HtrfcIcHBRa101C+ClUUWABsGELpfUu7lvjnb0IOER1NE9DJRNLatejLdKZ+zz2lT4gmo7GtigOiNW9OpefihGKkBu8ICckT3Gx9/P adamwick@ergates

93
tests/ssh_keys/rsa8192b Normal file
View File

@@ -0,0 +1,93 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
00UAAAAQYWRhbXdpY2tAZXJnYXRlcwECAw==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAEAQC+hQbPKvQ/05nzMyC8/3t5HOAlDtdCvlYD9/+BEYT98pn/iQtlLzYj4yA7BwjTNXNG6l1Z3MZz6TTbLXUl5SJlNWwt00G4jHttCVmpX0NoKIAd4RMIGRFw2uXmjq0qz5Fvkupctaejyrsf6P4YNktuqp+Znhxpz1SgfsXAiO+gSev+i7YCwVl0vuV6KTsUrqBtj2l7cEgfbJSo1zvII40WojouX2u4BOTFp8oOG0asnVpbsicqC/9TNgXUypH/WsjCNpL8//iqAUJb5Ptcje8yVQaOU4dNerpqdNNxKZO5TGWNKzXAaRLel0oreShLp2U7FDFRpq0MfRLg+/+krvqFmlDgnrK8NUikuVsJarHDTBTaYhxbpSyRiZkcUptzhdnI1kvJTzq4LZs2LOQhvomJ0qw+XJL4tuucjGtgckIlqRxEnAukCydSHYsKtDOurW/AK37kBf6hhCxiFrGncNX410M4ZYD9pNe7ix03XqGHfEuVVt1rB/493yw08phDVdEXIZvWxs8/xLYuYJW81Ph+bsVx/2Od7vbuA9x4BT2+Yirsu20mH9BtBudKxpxrIncHujuGfVsBtESkw4x9am4X7DSUwVQEoSwYHWsPVNGyJjayq+hh1mLJZINMxnxqN2Sz/YhT34creDYFVKkdXDM63wxQe480hdaENuxlLGjErJmqGoImbX6GjyI2jCwd5AWVsOV5mF1QEfCI1yZCyUNCFFd5CGZHswtPhwyPk+xcgAWhHlIB9FAG+qlx/ISA3jzO9W9+yZGghcD9ZexOs5yGkbEoEdxanl3aPNs5JfmehNLWE3Hq9yg4s7iPpH37pnUr0M1XrlExDvG1EO/T6OObwTujFUmlzNSUP/jFpJQnjGvFkeZXaw3pM5r/lLee/0oWUeS6JOEB1qMtP1UZJp1bwJvscoJmfTVLTK3ChR8/rOQqZzRzF5h0uma5HHVeKzzlCUt3qApdwkMtHjvAm4EbKBD8Jis1qQXF9pyfcdZeniPaIPnkeENXDy2Eo/mv83VPZcfm5S7cpLikmzLApfDy52ZEVPsJqmxAx5j80nlONnfwdmtjFxBeisB5uW8fdCLMKbQHT2O0U+aRpbVP2ScEaV6M6G+VahHIrqsmAFhc8kmpvCgH5R9ZoPEAWvSRwqHxuLSgtQXPGNQ/Om7HWJ2siqmN6sm9mu5AN1TEXnDJZN73PLFEgjjSf5Tf4lhlb8KeVHO6qiYAOH9W6pFeuoa1/UQD21I+LSxf+oAmsh+VM9og8hQrGqibXDyrkK1rps9I8ULS3f76m/yHc3yl3qZ1TF8IaZ2J9CBGxH8h++vE/DrztyokGVRpLaW2zi6z0FpXGY/cHWHJa2vDDTY8dsz9 adamwick@ergates