diff options
| -rw-r--r-- | oxish-proto/src/key_exchange.rs | 2 | ||||
| -rw-r--r-- | oxish-proto/src/lib.rs | 1 | ||||
| -rw-r--r-- | oxish-proto/src/openssh.rs | 92 |
3 files changed, 94 insertions, 1 deletions
diff --git a/oxish-proto/src/key_exchange.rs b/oxish-proto/src/key_exchange.rs index 17b056e..608dd29 100644 --- a/oxish-proto/src/key_exchange.rs +++ b/oxish-proto/src/key_exchange.rs @@ -624,7 +624,7 @@ impl Encode for TaggedSignature<'_> { /// /// Leading zero bytes are stripped, and a single zero byte is prepended when the /// most significant bit is set so the value is interpreted as positive. -fn encode_mpint(value: &[u8], buf: &mut Vec<u8>) { +pub(crate) fn encode_mpint(value: &[u8], buf: &mut Vec<u8>) { let trimmed = match value.iter().position(|&b| b != 0) { Some(first) => &value[first..], None => &[], diff --git a/oxish-proto/src/lib.rs b/oxish-proto/src/lib.rs index be7b0ea..ec28fd8 100644 --- a/oxish-proto/src/lib.rs +++ b/oxish-proto/src/lib.rs @@ -25,6 +25,7 @@ pub mod key_exchange; /// Named algorithms, services and methods, and name-list encoding (RFC 4251 section 5) pub mod named; use named::PublicKeyAlgorithm; +pub mod openssh; /// Protocol version exchange identification string /// diff --git a/oxish-proto/src/openssh.rs b/oxish-proto/src/openssh.rs new file mode 100644 index 0000000..f904b4c --- /dev/null +++ b/oxish-proto/src/openssh.rs @@ -0,0 +1,92 @@ +//! Encode Signingkey to OpenSSH v1 private key format + +use data_encoding::BASE64; +use zeroize::Zeroizing; + +use crate::{ + Encode, ProtoError, PublicKeyAlgorithm, crypto::SigningKey, key_exchange::encode_mpint, +}; + +/// Encode SigningKey as an unencrypted OpenSSH v1 private key file +pub fn encode(key: &dyn SigningKey) -> Result<Zeroizing<String>, ProtoError> { + /// Block size of the `none` cipher + const BLOCK_SIZE: usize = 8; + /// Check value, written twice at the head of the private section + /// + /// OpenSSH compares the two copies after decryption to detect a + /// wrong passphrase; the value itself is not meaningful. This is + /// ASCII "SSH1". If encryption is ever supported, generate this + /// randomly instead, to avoid placing known plaintext at the + /// start of the encrypted blob. + const CHECK: u32 = 0x5353_4831; + + let pubkey = key.public_key(); + let Ok(seckey) = key.private_key() else { + return Err(ProtoError::InvalidHostKey("key cannot be serialized")); + }; + + let mut private = Zeroizing::new(Vec::with_capacity(512)); + CHECK.encode(&mut private); + CHECK.encode(&mut private); + + // The private key shares its leading fields with the public blob + let mut public = Vec::new(); + match key.algorithm() { + PublicKeyAlgorithm::Ed25519 => { + if pubkey.len() != 32 || seckey.len() != 32 { + return Err(ProtoError::InvalidHostKey("invalid ed25519 key")); + } + + b"ssh-ed25519".as_slice().encode(&mut public); + pubkey.encode(&mut public); + private.extend_from_slice(&public); + + 64u32.encode(&mut private); + private.extend_from_slice(&seckey); + private.extend_from_slice(pubkey); + } + PublicKeyAlgorithm::EcdsaSha2Nistp256 => { + b"ecdsa-sha2-nistp256".as_slice().encode(&mut public); + b"nistp256".as_slice().encode(&mut public); + pubkey.encode(&mut public); + private.extend_from_slice(&public); + + encode_mpint(&seckey, &mut private); + } + PublicKeyAlgorithm::Unknown(_) => { + return Err(ProtoError::InvalidHostKey("unsupported key type")); + } + } + + b"".as_slice().encode(&mut private); // comment + + let padding = private.len().next_multiple_of(BLOCK_SIZE) - private.len(); + for i in 0..padding { + private.push((i + 1) as u8); + } + + let mut blob = Zeroizing::new(Vec::with_capacity(private.len() + public.len() + 64)); + blob.extend_from_slice(b"openssh-key-v1\0"); + b"none".as_slice().encode(&mut blob); // ciphername + b"none".as_slice().encode(&mut blob); // kdfname + b"".as_slice().encode(&mut blob); // kdfoptions + 1u32.encode(&mut blob); // number of keys + public.as_slice().encode(&mut blob); + private.as_slice().encode(&mut blob); + + let base64 = Zeroizing::new(BASE64.encode(&blob)); + let mut out = Zeroizing::new(String::with_capacity(base64.len() + 128)); + + out.push_str("-----BEGIN OPENSSH PRIVATE KEY-----\n"); + let mut rest = base64.as_str(); + while !rest.is_empty() { + // base64 is ASCII, so splitting on a byte index is safe + let (line, tail) = rest.split_at(rest.len().min(70)); + out.push_str(line); + out.push('\n'); + rest = tail; + } + out.push_str("-----END OPENSSH PRIVATE KEY-----\n"); + + Ok(out) +} |
