well, tests pass now
This commit is contained in:
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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
2634
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
54
Cargo.toml
Normal file
54
Cargo.toml
Normal 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
1795
specifications/rfc4253.txt
Normal file
File diff suppressed because it is too large
Load Diff
283
specifications/rfc6668.txt
Normal file
283
specifications/rfc6668.txt
Normal 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
787
specifications/rfc8308.txt
Normal 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
507
specifications/rfc8332.txt
Normal 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
317
specifications/rfc8709.txt
Normal 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
196
specifications/rfc8758.txt
Normal 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
1028
specifications/rfc9142.txt
Normal file
File diff suppressed because it is too large
Load Diff
272
specifications/rfc9519.txt
Normal file
272
specifications/rfc9519.txt
Normal 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
39
src/bin/hush.rs
Normal 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
4
src/bin/hushd.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn main() {
|
||||
unimplemented!();
|
||||
}
|
||||
164
src/client.rs
Normal file
164
src/client.rs
Normal 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
298
src/config.rs
Normal 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
205
src/config/command_line.rs
Normal 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
423
src/config/config_file.rs
Normal 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
48
src/config/console.rs
Normal 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
22
src/config/error.rs
Normal 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
95
src/config/logging.rs
Normal 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
304
src/config/resolver.rs
Normal 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
272
src/crypto.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
24
src/crypto/known_algorithms.rs
Normal file
24
src/crypto/known_algorithms.rs
Normal 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
251
src/crypto/rsa.rs
Normal 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
1
src/encodings.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod ssh;
|
||||
10
src/encodings/ssh.rs
Normal file
10
src/encodings/ssh.rs
Normal 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
195
src/encodings/ssh/buffer.rs
Normal 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());
|
||||
}
|
||||
844
src/encodings/ssh/private_key.rs
Normal file
844
src/encodings/ssh/private_key.rs
Normal 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")));
|
||||
}
|
||||
816
src/encodings/ssh/private_key_file.rs
Normal file
816
src/encodings/ssh/private_key_file.rs
Normal 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)));
|
||||
}
|
||||
448
src/encodings/ssh/public_key.rs
Normal file
448
src/encodings/ssh/public_key.rs
Normal 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 { .. }
|
||||
));
|
||||
}
|
||||
204
src/encodings/ssh/public_key_file.rs
Normal file
204
src/encodings/ssh/public_key_file.rs
Normal 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
9
src/lib.rs
Normal 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
1
src/network.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod host;
|
||||
211
src/network/host.rs
Normal file
211
src/network/host.rs
Normal 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
47
src/operational_error.rs
Normal 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
8
src/ssh.rs
Normal 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
428
src/ssh/channel.rs
Normal 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
173
src/ssh/message_ids.rs
Normal 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
3
src/ssh/packets.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod key_exchange;
|
||||
|
||||
pub use key_exchange::SshKeyExchangeProcessingError;
|
||||
332
src/ssh/packets/key_exchange.rs
Normal file
332
src/ssh/packets/key_exchange.rs
Normal 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
392
src/ssh/preamble.rs
Normal 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
113
tests/all_keys.toml
Normal 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 = []
|
||||
53
tests/broken_keys/bad_info
Normal file
53
tests/broken_keys/bad_info
Normal file
@@ -0,0 +1,53 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
9
tests/broken_keys/mismatched
Normal file
9
tests/broken_keys/mismatched
Normal 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
9
tests/ssh_keys/ecdsa1
Normal 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-----
|
||||
1
tests/ssh_keys/ecdsa1.pub
Normal file
1
tests/ssh_keys/ecdsa1.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdHAJM1xYjBZ7UdX/Gbgh3oNyHnieV8FGrX/kNE52fYBWbhlhKPySMcjyUOp/HNdnrqt4ckBx5yY2uB3iCB54I= adamwick@ergates
|
||||
9
tests/ssh_keys/ecdsa2
Normal file
9
tests/ssh_keys/ecdsa2
Normal 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-----
|
||||
1
tests/ssh_keys/ecdsa2.pub
Normal file
1
tests/ssh_keys/ecdsa2.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE/n2mCAHZDC0V1sX473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri93RQaw1U+yQImCKvhv4uEDn0tf2BuOHI= adamwick@ergates
|
||||
10
tests/ssh_keys/ecdsa384a
Normal file
10
tests/ssh_keys/ecdsa384a
Normal 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-----
|
||||
1
tests/ssh_keys/ecdsa384a.pub
Normal file
1
tests/ssh_keys/ecdsa384a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBA2ji0KYFboMm/ZeyGjDHkSybxIvyJeBI2Cgw6/xP1/W6kmc1nQ00GrQNOTYQgCUYWMVEbBjUsy+5cISM9w2A2BsuRGm5IFkSlR0K/u8/5DgLADBrpkuD9tQ7EzovfEP3A== adamwick@ergates
|
||||
10
tests/ssh_keys/ecdsa384b
Normal file
10
tests/ssh_keys/ecdsa384b
Normal 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-----
|
||||
1
tests/ssh_keys/ecdsa384b.pub
Normal file
1
tests/ssh_keys/ecdsa384b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBIW4E9sbHIifQrudPuAu3WY75E/tO5LMVVq1KYpLtQOXwRoG8QdIKplZa+lrPWHDaXt8Q1qR2HZoCYbhlCJBTaT0svM4lSZC+QnOLHR3fkaLIe+uAJuT7eO7HSdEWqN/UQ== adamwick@ergates
|
||||
12
tests/ssh_keys/ecdsa521a
Normal file
12
tests/ssh_keys/ecdsa521a
Normal 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-----
|
||||
1
tests/ssh_keys/ecdsa521a.pub
Normal file
1
tests/ssh_keys/ecdsa521a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFomjesoxYmpKpy5iRATGm/EZ7NtmOSEKa3bpi0oIHFPIj6m2BXTA+6Nk7G4D6lhj3T1YaW6UocDOJw/x95Bl1SmgErh47C6eK3AQCmGvcjR2pwEAlZ3gogyi/XWpi9w1vyexRua6WTXT4hXElAv5vAeEux6ccCsVGUGs+Ie/7q2FWJsw== adamwick@ergates
|
||||
12
tests/ssh_keys/ecdsa521b
Normal file
12
tests/ssh_keys/ecdsa521b
Normal 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-----
|
||||
1
tests/ssh_keys/ecdsa521b.pub
Normal file
1
tests/ssh_keys/ecdsa521b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFhruf6BilNo/4Cpf6uEehgtvNIWMGdqbGxsiioC+FyoVeleoNPJ8/ltt77/GvqKIlk64DGvG3fRIPqvklHiWzjyAAoe6oQ6ymFK8qjgG5wBCgkt90PxeXbZEJE32WlPOG9uX2JhSfRhXWxwBTzEyai8FN2f1npCshiobkIw2fZ8ivhOQ== adamwick@ergates
|
||||
8
tests/ssh_keys/ed25519_pw_hush1
Normal file
8
tests/ssh_keys/ed25519_pw_hush1
Normal 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-----
|
||||
1
tests/ssh_keys/ed25519_pw_hush1.pub
Normal file
1
tests/ssh_keys/ed25519_pw_hush1.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8pW2btKRHc65DSyKjL7XBtvvPTHGd5KoI6L4KatxRZ adamwick@ergates
|
||||
8
tests/ssh_keys/ed25519_pw_hush2
Normal file
8
tests/ssh_keys/ed25519_pw_hush2
Normal 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-----
|
||||
1
tests/ssh_keys/ed25519_pw_hush2.pub
Normal file
1
tests/ssh_keys/ed25519_pw_hush2.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIET2SpCeHNG/3IILj80xTImmn2JEcTRVM4khSQ/rCQ3F adamwick@ergates
|
||||
7
tests/ssh_keys/ed25519a
Normal file
7
tests/ssh_keys/ed25519a
Normal 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-----
|
||||
1
tests/ssh_keys/ed25519a.pub
Normal file
1
tests/ssh_keys/ed25519a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDiR2pvyxN6DtqbBjxN1yTbMRupxcNEW9r/hVEVY1jT adamwick@ergates
|
||||
7
tests/ssh_keys/ed25519b
Normal file
7
tests/ssh_keys/ed25519b
Normal 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-----
|
||||
1
tests/ssh_keys/ed25519b.pub
Normal file
1
tests/ssh_keys/ed25519b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ+TfFfGouGAFi+BTIAL68G77dcEP8JjxJjvCnaGteER adamwick@ergates
|
||||
170
tests/ssh_keys/rsa15360a
Normal file
170
tests/ssh_keys/rsa15360a
Normal file
@@ -0,0 +1,170 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
13aWNrQGVyZ2F0ZXMBAg==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa15360a.pub
Normal file
1
tests/ssh_keys/rsa15360a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa 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 adamwick@ergates
|
||||
170
tests/ssh_keys/rsa15360b
Normal file
170
tests/ssh_keys/rsa15360b
Normal file
@@ -0,0 +1,170 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAHlwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAB4EA8aXD9LdGBBkuAO1KR4wBQljIW/pX5Anp7SQaSLd5gD318SSRvNc0
|
||||
uR0y+aJX+V4Ft2g8Aowr9vqBI9Q9m8TDRw0WQFJ6PC0E+GMLR/XcB/6fEdzqExL1t/ih20
|
||||
CPCm86iY1GWAo/1nyzd7KDoX3wWiczPSfLsz6tG+NRMMlo3dfv7Pg92DRAFYNhWb2RPBG2
|
||||
WKLszs+DDyGPv20yGZj9c8LkmS6Naw3qCEXx7LAxdNZImwAWsbBTdP0rXKcroekalO62og
|
||||
sips5UR75uITluYHkE6HWD2exzVvTJa+hzbSgDFeLOMCPxlHQbDhW+6bsb+TOPcJxuRgVq
|
||||
SxYljKMwax52xBZ4LdXiJKxKwHYT0aLo/c/zTGVLwkynC0lDNPS+3Y7JA0O8wenrD+Fxwp
|
||||
aHb+DJutk1/FA8bGuaOZbmTrQqHbkHxzMS7qB0nAmyP99uf/y3zpO/P3LKF2hJ3Y7NFvMl
|
||||
ivmYCNQgDOsQlQZV+Rlv4eAODvSjPS232DLNAAcZvd/68l+E/1e0bHTrHdFBUoJD5KYouB
|
||||
lCQj2XYaFMx5YuhiD1mVHB74srxIE7Ktn6xFgohzIK8m2/ec1Mk7IbEhFJALA2RUe0nNLY
|
||||
pTHfeuLgN3PmNH0GEyrBYhqzPkMLY6U8PE1tGh6Aa4B7S07qa9FolhbyfY/4Y3J2+3FGnE
|
||||
6ELEe+VFncFF4rLM2OwTXP9cmEwY1HwxNsPyY9B34jWVDiciAGyW+qACQ38tOzFCKr20GL
|
||||
jERsECp/l/gYlpoXIrj4oK1GZZAVBT1UHlXOf5VR+XzQaiG8rE9jXm67xpotJ6zjKMBdWN
|
||||
GYGotqE876QHxZvaCFLkeNwZb5UDVuH9LZ5HRdQcTgVo1ceSmhf15mjmdfnCFKW5Fz4KAC
|
||||
Wsd0g8IlLtMlOkZQldO9hXEPC+5h00dC3OteQLUqUc7HX4YLs5ftsTdUwqXNocFW6SjhtQ
|
||||
fHlWxUzCE5lPNd2+Fd3v1D1v07AKAdz8+j7f0eAtEftuOQjzasrCCWWmUOIwVtq/2OPcVb
|
||||
i269vxxxmoY3ix4DkbwQHFAs688q8RQtzHw9drcfFf5zjKiNGSqfEcmfn1Fb0ElpbctzQ8
|
||||
VgOoaYWjBNjIVaE1MfiNHIxlAX+YNmq+H1Z8bxlGRTh5H4vCP+ZVHpgH0zwt/hWIKqey/D
|
||||
PJimHa2uSqUsvioF70HbU1PYdB+0uXusmnmwHEj0Yzi1KO4eRTGjfLVJJNsdsWpoBdUnsh
|
||||
zfBRNXDB5lj+IwDClAseyA/DY9HDOS/29yoU7vF9EqveJPVhE2yvAxBNlVKfdODvktX5wd
|
||||
aLYjkNhpUPXl/PQM+iETFC8Oe2ldFCgNgdb1O7O9a8v2mH5QtkPM8xFLQrZc5FCZ6YdECH
|
||||
aK4dCHvps1ccEu5/ljR89lYNGNLtq0rgyo0fQeVgKkF8cMidbpPPegMhR9dO9hEX+s9xhJ
|
||||
3jrB63O3519pHd0XN9/CvJ6WgZICiKvPvXBYZItrgLhltgK4W4NHdUezD1YxGxlZVlq6mT
|
||||
ljVbbP2OjRY3V/Yy6fbiMrL+1RTVGeGijJO2iQa9v2kh01oyd/i5gSOcfe82WutFpuFHxz
|
||||
+47yX8E2HGfWIHRctMDehUVFksXvajC0hhQ9aW4TyDMB1VoK3pW0jIKppZegdeZAINd/NM
|
||||
qCcGZ3MiaXzuYVeAp386GgEiCT2v363ZFuJBskmbM8Ap8jPNyJ2aUNDaAcAc08lEyHzYhl
|
||||
pn45bGdf4GGj+PoUHKFqJgnGk4vGRsmohEazVaLMPo69Oy+rVoZN+IT7wiUkFsZe/yv4io
|
||||
LXdYZpNuyU5aTkl5JgHgF0NCVYngkcoOHHJM2/DranBarSMhCz6KvqySNi2KxLG5PezwCW
|
||||
74wMHMb1sZF4mFbRRmQbKnaDpQI4RRWwqfZ5bfSbotPt/SWxeO057dMRMsnX4sPCH93BPo
|
||||
4f1cQR4GpKQCya6cm/CAsXGH9nixPSaj9FSXjtQOvlVVJAm0Ye/j//LjmRUnOT+yKP8Qdq
|
||||
SbdbAssq77KcDVDX3l1cfxfP/5sgNj0QYvDUh69OH16N86L2CBPX7P+U4mWKl5brxgyshV
|
||||
4aBU3e/IyTbYv59W3ZlgkyHRhK1lrWx9qYMex+Eq255qzZUt+xeqkBZGIzC69QlbcOVZHY
|
||||
GtqhvROSGBX++CgkizXuazvsE+Fv6exhi3OT7lf/dvdJkTgOSUbGT/X+spxKMxuGqMGhZn
|
||||
nNNxKRANlIXyzJ7W1YZs7mZgiKHzhvPcSVlKKwsa3WN/DN5pBFD7LwRbk+bu/0kJg6mGA4
|
||||
c4+wCITPdNypmYLZ4iMSqbqG2VvmvTTtERm1Lu8KCV87050hnwCjfpzs6x8ibxMwOjXF93
|
||||
rJmTsd6AciKQhbGlNB33gfyoAtuZ0PBKCPrfdV/JO6c5BD4Y4QquL1fr0ullFXtrNRVxmx
|
||||
zMleZx37Dsk+UxbUmYaXFaYxMADaSM1U9hsg2FwZjSslgArPmVdZmgKxqeiFZhVC+FwdKW
|
||||
cMtA2/Cn2wbOWsc6kh5BWeStW1EGUPR5mLOPJABtmLSbFSeqGOqYIhrph7AAAaiEwW+fJM
|
||||
FvnyAAAAB3NzaC1yc2EAAAeBAPGlw/S3RgQZLgDtSkeMAUJYyFv6V+QJ6e0kGki3eYA99f
|
||||
EkkbzXNLkdMvmiV/leBbdoPAKMK/b6gSPUPZvEw0cNFkBSejwtBPhjC0f13Af+nxHc6hMS
|
||||
9bf4odtAjwpvOomNRlgKP9Z8s3eyg6F98FonMz0ny7M+rRvjUTDJaN3X7+z4Pdg0QBWDYV
|
||||
m9kTwRtlii7M7Pgw8hj79tMhmY/XPC5JkujWsN6ghF8eywMXTWSJsAFrGwU3T9K1ynK6Hp
|
||||
GpTutqILIqbOVEe+biE5bmB5BOh1g9nsc1b0yWvoc20oAxXizjAj8ZR0Gw4Vvum7G/kzj3
|
||||
CcbkYFaksWJYyjMGsedsQWeC3V4iSsSsB2E9Gi6P3P80xlS8JMpwtJQzT0vt2OyQNDvMHp
|
||||
6w/hccKWh2/gybrZNfxQPGxrmjmW5k60Kh25B8czEu6gdJwJsj/fbn/8t86Tvz9yyhdoSd
|
||||
2OzRbzJYr5mAjUIAzrEJUGVfkZb+HgDg70oz0tt9gyzQAHGb3f+vJfhP9XtGx06x3RQVKC
|
||||
Q+SmKLgZQkI9l2GhTMeWLoYg9ZlRwe+LK8SBOyrZ+sRYKIcyCvJtv3nNTJOyGxIRSQCwNk
|
||||
VHtJzS2KUx33ri4Ddz5jR9BhMqwWIasz5DC2OlPDxNbRoegGuAe0tO6mvRaJYW8n2P+GNy
|
||||
dvtxRpxOhCxHvlRZ3BReKyzNjsE1z/XJhMGNR8MTbD8mPQd+I1lQ4nIgBslvqgAkN/LTsx
|
||||
Qiq9tBi4xEbBAqf5f4GJaaFyK4+KCtRmWQFQU9VB5Vzn+VUfl80GohvKxPY15uu8aaLSes
|
||||
4yjAXVjRmBqLahPO+kB8Wb2ghS5HjcGW+VA1bh/S2eR0XUHE4FaNXHkpoX9eZo5nX5whSl
|
||||
uRc+CgAlrHdIPCJS7TJTpGUJXTvYVxDwvuYdNHQtzrXkC1KlHOx1+GC7OX7bE3VMKlzaHB
|
||||
Vuko4bUHx5VsVMwhOZTzXdvhXd79Q9b9OwCgHc/Po+39HgLRH7bjkI82rKwgllplDiMFba
|
||||
v9jj3FW4tuvb8ccZqGN4seA5G8EBxQLOvPKvEULcx8PXa3HxX+c4yojRkqnxHJn59RW9BJ
|
||||
aW3Lc0PFYDqGmFowTYyFWhNTH4jRyMZQF/mDZqvh9WfG8ZRkU4eR+Lwj/mVR6YB9M8Lf4V
|
||||
iCqnsvwzyYph2trkqlLL4qBe9B21NT2HQftLl7rJp5sBxI9GM4tSjuHkUxo3y1SSTbHbFq
|
||||
aAXVJ7Ic3wUTVwweZY/iMAwpQLHsgPw2PRwzkv9vcqFO7xfRKr3iT1YRNsrwMQTZVSn3Tg
|
||||
75LV+cHWi2I5DYaVD15fz0DPohExQvDntpXRQoDYHW9TuzvWvL9ph+ULZDzPMRS0K2XORQ
|
||||
memHRAh2iuHQh76bNXHBLuf5Y0fPZWDRjS7atK4MqNH0HlYCpBfHDInW6Tz3oDIUfXTvYR
|
||||
F/rPcYSd46wetzt+dfaR3dFzffwryeloGSAoirz71wWGSLa4C4ZbYCuFuDR3VHsw9WMRsZ
|
||||
WVZaupk5Y1W2z9jo0WN1f2Mun24jKy/tUU1RnhooyTtokGvb9pIdNaMnf4uYEjnH3vNlrr
|
||||
RabhR8c/uO8l/BNhxn1iB0XLTA3oVFRZLF72owtIYUPWluE8gzAdVaCt6VtIyCqaWXoHXm
|
||||
QCDXfzTKgnBmdzIml87mFXgKd/OhoBIgk9r9+t2RbiQbJJmzPAKfIzzcidmlDQ2gHAHNPJ
|
||||
RMh82IZaZ+OWxnX+Bho/j6FByhaiYJxpOLxkbJqIRGs1WizD6OvTsvq1aGTfiE+8IlJBbG
|
||||
Xv8r+IqC13WGaTbslOWk5JeSYB4BdDQlWJ4JHKDhxyTNvw62pwWq0jIQs+ir6skjYtisSx
|
||||
uT3s8Alu+MDBzG9bGReJhW0UZkGyp2g6UCOEUVsKn2eW30m6LT7f0lsXjtOe3TETLJ1+LD
|
||||
wh/dwT6OH9XEEeBqSkAsmunJvwgLFxh/Z4sT0mo/RUl47UDr5VVSQJtGHv4//y45kVJzk/
|
||||
sij/EHakm3WwLLKu+ynA1Q195dXH8Xz/+bIDY9EGLw1IevTh9ejfOi9ggT1+z/lOJlipeW
|
||||
68YMrIVeGgVN3vyMk22L+fVt2ZYJMh0YStZa1sfamDHsfhKtueas2VLfsXqpAWRiMwuvUJ
|
||||
W3DlWR2Braob0TkhgV/vgoJIs17ms77BPhb+nsYYtzk+5X/3b3SZE4DklGxk/1/rKcSjMb
|
||||
hqjBoWZ5zTcSkQDZSF8sye1tWGbO5mYIih84bz3ElZSisLGt1jfwzeaQRQ+y8EW5Pm7v9J
|
||||
CYOphgOHOPsAiEz3TcqZmC2eIjEqm6htlb5r007REZtS7vCglfO9OdIZ8Ao36c7OsfIm8T
|
||||
MDo1xfd6yZk7HegHIikIWxpTQd94H8qALbmdDwSgj633VfyTunOQQ+GOEKri9X69LpZRV7
|
||||
azUVcZsczJXmcd+w7JPlMW1JmGlxWmMTAA2kjNVPYbINhcGY0rJYAKz5lXWZoCsanohWYV
|
||||
QvhcHSlnDLQNvwp9sGzlrHOpIeQVnkrVtRBlD0eZizjyQAbZi0mxUnqhjqmCIa6YewAAAA
|
||||
MBAAEAAAeAYlonoYietLhS4wmxe+Fd+dUM53LDJwtp7J0PHZ2flDSjz1wk/QlSai2aO8R5
|
||||
rgM4rGd+VUMb+dAHk7+ku6ugF2EaN1/aZHemWDpnswg8X/ygXbLeipji7dgCeKyUC5kt6C
|
||||
JaCSdSyEfE++jqbmZF10uxLSjvXasa5gjlWMgBKJnlCzwWX9MUai0pCE+Bt0M2Rmk5nQsU
|
||||
uqncSft1srl0HxOp2zb5VCM7p9ZgGwezeWxl7MBifDvaG/mXFoTr22B28zsdlmKV3fKIlx
|
||||
LI3Dj11cor1zlNSvtUDoZfHM5lfH4Wk2fWp/1ZLCT9hgQPyi3futPjg+AHefRmSN1gtxcM
|
||||
c+zYRgMnMvCktGxzmFX2xxJZZkSnL+biqNht/Mf61KjwrliZM/zz7LD6fWIy3RJLWZvSP3
|
||||
x83o8BqNc61Em4vzvREHvo0IjXIcyo6YGAzUJxRSJk5W15H6fm3RQTTFv82WRpWWExIhbE
|
||||
XL2n6B/GCjbyNKruzeOANTxQYWx7x5EcRw3Mo62BAjR+OM51i5NJ1P00CPIgDJ4rO6652I
|
||||
DAMVPM58aub5K5LngkfjxjCpPh3txK0ovprLZCgp6ulkadggMLBX6y+AuxVUrz5nigDvIc
|
||||
dDV18tMYD+ENCJL5dVPwZMQx+hFBYKoddqO6ivI5s1xuBvicBcL9Q+yoxLpcFO5YGXWx37
|
||||
8bNicFx5x6h+URGWbF48lO9fUHz8QOfug2Fvo20GB8oqwSMzCKnQjREFb0P68zzwliOkox
|
||||
2Haf+1wIIpfKigs8ZcX46EMH47jk7USnMrY8VZYZpsBBH4ROZQ0HZ+iUJFf3JllYYSVxLV
|
||||
LGxExYcjfNPQLejXaXTrAQfF9jU/qQol4xOMkcZCwvkRmpADjOz0s1aoOO+FPn4W7g22e/
|
||||
nolN06Qe1Hxz3MYha6fApS+R5TzfBdM2wEk7GIQEazphAgoVM4wsX7PDXe7HvtjFwOlwsg
|
||||
yr1RYgk4fsnv/SBsTWqPCCcbx/ajPlbRwUnmmMwt1r9jrlNzF+SX7CHoh6xmV3Vw7hp9E5
|
||||
47zRCoxSw5QZsoxnXCrbvXeLFHlUwubRfjAsc4l+tEmGqMS5dSy2A9Z9VLTD20eQXQG/LK
|
||||
YQSitUeB0S7qsD3sNmnqV+umKjcXOli9IxtTISXPEBb9ehBCzxTlW08ENug+jyu80df2Lb
|
||||
V4JW7adI9xKE5CtyJAFrmzrdC5qPtVN0NJY2FcylAWCsu+tFM/0C3t0CLa2OFI5vry8p9M
|
||||
LCb4eOKsT3kfNCpGSnBr1vDQwdSc5H85lPcxg1yeGZVpFe3AJ0R9IW5pa3VHQ0An3aw2S7
|
||||
Ehr/KkaSvbMyreh5YHfuJx7kCOcQ3DOjd/XrMx5cDbGmjuqsxeThkGJhBSgyZVjdU+uUXp
|
||||
aYk6aHuC6EVBsfLdkwLAS5elii/1VS6ugWDkCRrrBcRlivANfeZxRYEr2lPhUNinm+OQUC
|
||||
lyDo5HhrK755xQKqUcAB+7plRNiAaw0KPh14VqhnOxxxY4/6S70tqbSXJoU9ysi53fo4o0
|
||||
1DobdBRxVkF7dGTPSygnjGR2MjmBVhySie6Uuk0ClCxU+ruM49jmjrIW8mdq7ntAysmeYV
|
||||
2s8/xs+cJWwNMJwEy8DNk/HpYlyYf/2mWMj8RaIk/Ks87Yk07YYCAgOzeOAYsqyKdUZAmJ
|
||||
fOlWqBDQd58qJ9Ipix2gYU8PNevDfSZiQZUWOOrRxj6wSawuRZrtU+esJQpytURRynfxGI
|
||||
c3Ae8fBfZ8PAV0BmI0bXF8Wmfl6mWQ7MNWe1cG2F5TN2vSMkm6SweCgtzv9ijcw3B6qWX5
|
||||
ZqkEYquuFguyonB/vWWfK+G9oTimd3duwAaguuNSgh5zoZiY/tGERisFenYxZdihO4sXUX
|
||||
J4HaYumPyNVS/A7MWm6ZRWvlUM/p8xYWz2T7vH4gRw/ZdECv3KYUIJZxscLzlEvXLxcUpr
|
||||
OBXJECMosgn4f/3FAy9L9XMFkTe03eDLD7YKUfrWBwpfA70l3MDhdfLS2971fDO2ENffdW
|
||||
TzZ4K6YdMU3JofHfFUtSTY+94gKW+pxM7d/cObic8Sr/g1YGKP0SNQTcrD8ZwXdOifXidk
|
||||
ppskNLwCJgSqRgeBiINPnYyv2lotmTSLF8lYRMD74sSSHg6bljRr6gy2gI3Q2kFrHW01nM
|
||||
0i/+EleuLzwPodHPZ+iKU2+an4vTNHxiMCM3tCMN72FiaZXqvRinfv4rYS00KiiZtW2A8d
|
||||
YoFsoiSzP0xM5B/HvOevm3G3hMWR8FWtElYKuGh9Rs016jy3qnWyFHaFhgTeX9zfip6ijR
|
||||
VwDJ/CRA+9AZHtj6CcASt6HZoFSMqXz7ef0aeFy/2LTsAlGWbHH/4TKRms2by5+LzJ4YWC
|
||||
T6vI4bNkHWqQYgxbcE8rMtRwv4OJRzGJZ7o//mbObMuUE37dqlKDRAwx/8L1vdoFxFJCFG
|
||||
z+aEo4P1nxs0EiDNOcxGKMuKhjiMvlh4uTJXw7KycORM8LL9IbdQzPp5KyJn2X3L60a8vu
|
||||
pXMloVpBOEMNk5ez4x4DAZOz7/dWpOEEFnOikhhaHHi2NYqDPRAAADwD3LgxLd4ZSFJlIy
|
||||
GCa16WcnUrK14IiKW5SLmK1wbNePmvPS9BVw4FRztnan+9IrZlP8ANFMQHyg4UXrZvNi4M
|
||||
RsTsl70+IbzsnbvMGCPizPEzIVivB2Qnfd5/XZynM2vA/S8CqpXmwCmqY+EC0YJQzkhy84
|
||||
/WBsmWmWT2QwHV1e6ni2rAftKby6r2EOUOKGJAteH6pakHTto8qSOKsqV9Qy38nwZhQc89
|
||||
UxxJOdufsBpe7+BFsqREypxPZWq+4DciHg6Zjr/S14D5GwqxpAcp32p3O9+JS+I2VyOjMQ
|
||||
6tpR9h0rcgq5uX8DTxrEyWxcEjK60a31m0d+Y9BKoUnzqkqRRN+2+YdWocOm7KpeZ93LBg
|
||||
ZGVnAmIBrsQxNaDij00MmCd9eJyVfw4x0GO9BKcRaENNWv9qGqRLPgBbMFKnO1PJGKiOCM
|
||||
e/nffAhzA9Q3845Sdyn0j/KabuDsLoN3/A8iRGgyNDGqYfFo1tQDhQdZaKe1QECyoe1iRx
|
||||
+Y9AJiB7t9C/EkNJuVtP6CnUxD01Ei1kKGQoP3z5yx/rsWAsPBnDdMiaZZi7NUm+GwXFnI
|
||||
p4/eW8udcmC/OtR0u8/gnQgpwj68nbmklNHzR3zgVfFKplXa+my4DY4Nuh1QDL9Ts8YgRp
|
||||
CUjPR03BTwXECoCnTNB/KYoOc1D8pDqp2vWbsKg2JWDqMnfWMemoQH9E+eOuSTN8oGUSNp
|
||||
ac//gZfOh9jl7pSvrw7+G1gaCbA5NniCBtB3LnG5AGZO2H5NO0UljatqYabIQ+Ipgm+kYm
|
||||
4SkukANONTZie06QH/uyul2fokhV9iHQs2/77FJwgJHOe1hAaNS2fx0+Zi0uUgrcyeeVHq
|
||||
zyIaTH3NUs1msfWrYnHLWJvWe92de5u9fxAqUckLbiX0lMvCJYk/x9OSWiuwxCqRvySpex
|
||||
tWMG7fL00tFfSQ9d7lbR72Vx1cckeMvm5UxS6pjl8f5XPT3+1vTRBoT7+4JaRCTb6YalqW
|
||||
adkZBO/tpJaz0G0rSmimevqbaYDAcKqWGM7ZPt5p80wixLHAE8Dt81x3xK7iQbCB69y3vu
|
||||
NgGsydcENIkEqHVxa/XTxTt1C5aoJqQIisDdnRCEk8lwW5nh+r5zB2y80V3MBOXdym5Tp6
|
||||
Z5Aqef5tFUB+EZULd/2bM1puXdeiJ+vokbm6leUahQrM2UZVi6jeL/J7VQ73vQbWMcEzT+
|
||||
SXLirclzxsJ+H3/mcnYXJROms2aroTbwAbqvo0r31JNUWtOGfojW6TI3xym/MQIyi+bPlY
|
||||
6zMPlgAAA8EA/PULCYaTGRSDGyL2LLTJAxdgZdfEwQd/zlAj7dD5Kq7mSlw43gJEK3VMyk
|
||||
cJ0jqFp4dKdGw8ugaCmUKkpKPXW1C9hdSn04WUBELjFmQ4VTbRZ/8YKhq14sIyu7nRrdF9
|
||||
+zFSWhnjNAFHmIuYDq3EtCONh9MQXOHMrGvstAhBOMVM0ga2hN4KyctyxRLh/D5dkkNw11
|
||||
fI9YSldkwy7ewujKzbPQW3JKKuaLwyyq0KPEhj+I05V/t+dGVu7fYM7ieMTPvxo/bc6uIG
|
||||
AfDsJcM3k2jOnCp1gDbujVnKKu7EGJSh7MYPNZPQH7P+6FJ1Apy51pVVYt9QsN/w0T14kM
|
||||
0kJUyqmq41CRiXTY0Xk7a6+ckW1QKH8/wtDEO+KFyxI0J7ryAC1WLKlxuN7wbCkWortqHg
|
||||
wjXStWjkxLx902KXC68Sf7Qj53bDnccFd3vmzjiUCm3RJaTpCfrroXaPpBO3IJZIzRMsGR
|
||||
XgttigC0VEnvacSlzfZqN9HehtnMF4EA1O6Ri2Klku6+dm6KLEUn8wfThs/Y3VPYTBHDtw
|
||||
Ej2DDzlx0gAT7Gy/U5RirfL4OlRFqRpVqf+5w8I7ax+HTjobKqdZXkRTPA8Pl5zN34OpnK
|
||||
LYapc5xCmVEriyzKKdklgvBxT1lkA69Mz0SFdZx3U9sPYkpR4dqzzkxQkIWGVQPJ+Ys9A2
|
||||
CaM+7w7x8OdFfUz7E7n21BYcd0+vexPmxG8xc0jhgE1UzxHqJAGNKYGS7mV398Sm6mnlqT
|
||||
wK59SaWXov/I5G0paYRpIZlplAlxxeO0coZCJfyeDrN+H27wBISDQAoXUhH5eftArtdU8X
|
||||
o2ikaSeiH4qzEJYhf21ZGxx8jiBZ5qjuz1bc6B0DuLFERcu//76i27m6y+vz993wXtNbqY
|
||||
NVHzxQ7OSZ/9nDqM8HsAeadVtgKoZBm76sX2xYkid1cMVtDqOHUMdHwObFDQDOxyFQGAzD
|
||||
9B2FKe9NUslB2fOTutxvacGHB+Tf7zgI/eY6AWbdCI2EnoqEuBoc1mdGqhF5qKJoAzuD06
|
||||
Ds8efE7kK7AlwgsOSYjehSG5qkUiy418HjuOY/RaXSR3aoYbZraTRydITGfH8OsR7qIPRm
|
||||
T96nrRjmMdUi+7IUnyNiBWBVRclWJ0bB4TZ1Xhk2WtJteo8hpjiv9fHPSlBLFDou1dtr9p
|
||||
B/OQTR6Z7lh8PM3LIuRuW0KTsTf3gkJnHQiSs/eaVa9zu908RT7gsU0iEY/zKJF3+xw81h
|
||||
zRsHblj9eLnhBF8OBJPEhnosJ7bziHa9AAADwQD0jeUx8+IlnSONTU1zL2PDdjJo3sjZvj
|
||||
6aDeH8+vnr1Cnu2QreqNCMmVzfSOt7FGh4rSDj/WUieCn9zDdiNQXsCyW3VnRswWmFSCUo
|
||||
jaReaMNP9Yzz0PnUTh9Xvtyv+67LWVPnBGYkifTaujM6WK5wC4KlmudOxN2YBTz6WP9Vk2
|
||||
Pvl0FedBbWt0Lc6evK2H0bMCNgdYAx2e9qOzm31OnPVA3g4dhkUHqk/tfu514fnX08NU6U
|
||||
nmEylmWbGWW995ongVPaEobuKMNdivRnvlDhq3lsPdr1qKRzPiRp3t4koujoinSmMvjiF+
|
||||
t1gneoUlgQGteYmZFU76A1Mwh0uMRQPWCelNwM0PZwLJgirEA5Ga/PW7b7um+EEZesoFWw
|
||||
5KsfA/Ix4UX/otl2d6KhHRBhgSb368+3hU7GZtKO5SPT0nxWjUGxX9mJWNyeVDOgVV+aX2
|
||||
/bOltPFa3qpeD3SzMK8llPSDS/2/iAZ+dI6W6jvFFjnvYSZyac9CNXnmnaHu8GjaSdzMPF
|
||||
5DLq0AvBUAXZG+DMP9HL0zo9/NmxAN1b6RokOjJbZeqhylhlyQRDLZ7VH55BEU/JPLpqrS
|
||||
v1EuEPeWq4cqdIPdOCxgltGR88sC/ivI1jF3mzbvMDzSBDPmfPaveeq2LF5/9t3fX7MUcd
|
||||
u38WVGrQYmR2h1bgbGgW3k2d05MLSxvOG4A9IHN47JhuxkGnZmNmNAhY5a+pYAcLGFKBo+
|
||||
18S1Ufxd7lh2/edfi8Fwo7aNLeyM/GR1YZbc2+Noza6RHRy+hE8pv1Td077G1t1dkKjMjN
|
||||
aFP7rQo8Xt6STbl+7AIOBP6vpWsKimnCAiIoRYSUhXQihcHwrqYfNM+wTITTHvfVXX+rxH
|
||||
wQ44aItom79YieMlGQAGEdEyexGcTqL9zg6QzHIPDYTDrSepTvPXpFrjH0zQbOY8eTV9dR
|
||||
MtABXGCDB2hbTQ2Y8MBIcolcOlvcSivFBnrmAjeaEb9VNBYhLpguMxMP+qBliMYAxSOv5Q
|
||||
RfnuuFrNQO0r13x9tDG9jS5IFHPasKgREBNXuG6FtuAo0bhhaxSR78jSRV7W6+mQTdxDuK
|
||||
cG6R7S8f2p/fu3866I6FPEQUO3Y9xHgsbQHytk8OYR4jPkX9wfUOKvGjaRztEDPfPur8Ws
|
||||
wyzoV1klMg2id53XbGw4SzwhnAjipfotu/9v3r6C2ASJX2kr8ABn1MUFY52P9plHsGNqmq
|
||||
hgJCew5FA9CYM+Lhkvu5qk45X7Bag9nJoB4SbvdGqcpDok7pM0sd237PO5cAAAAQYWRhbX
|
||||
dpY2tAZXJnYXRlcwECAw==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa15360b.pub
Normal file
1
tests/ssh_keys/rsa15360b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa 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 adamwick@ergates
|
||||
49
tests/ssh_keys/rsa4096a
Normal file
49
tests/ssh_keys/rsa4096a
Normal file
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa4096a.pub
Normal file
1
tests/ssh_keys/rsa4096a.pub
Normal 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
49
tests/ssh_keys/rsa4096b
Normal file
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa4096b.pub
Normal file
1
tests/ssh_keys/rsa4096b.pub
Normal 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
88
tests/ssh_keys/rsa7680a
Normal file
@@ -0,0 +1,88 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa7680a.pub
Normal file
1
tests/ssh_keys/rsa7680a.pub
Normal 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
88
tests/ssh_keys/rsa7680b
Normal file
@@ -0,0 +1,88 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa7680b.pub
Normal file
1
tests/ssh_keys/rsa7680b.pub
Normal 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
93
tests/ssh_keys/rsa8192a
Normal file
@@ -0,0 +1,93 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
VtmGIwAAABBhZGFtd2lja0BlcmdhdGVzAQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa8192a.pub
Normal file
1
tests/ssh_keys/rsa8192a.pub
Normal 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
93
tests/ssh_keys/rsa8192b
Normal file
@@ -0,0 +1,93 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
00UAAAAQYWRhbXdpY2tAZXJnYXRlcwECAw==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa8192b.pub
Normal file
1
tests/ssh_keys/rsa8192b.pub
Normal 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
|
||||
Reference in New Issue
Block a user