Skip to main content

ssh_key/
private.rs

1//! SSH private key support.
2//!
3//! Support for decoding SSH private keys (i.e. digital signature keys)
4//! from the OpenSSH file format:
5//!
6//! <https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD>
7//!
8//! ## Decrypting encrypted private keys
9//!
10//! When the `encryption` feature of this crate is enabled, it's possible to
11//! decrypt keys which have been encrypted under a password:
12//!
13#![cfg_attr(feature = "encryption", doc = " ```")]
14#![cfg_attr(not(feature = "encryption"), doc = " ```ignore")]
15//! # fn main() -> Result<(), ssh_key::Error> {
16//! use ssh_key::PrivateKey;
17//!
18//! // WARNING: don't actually hardcode private keys in source code!!!
19//! let encoded_key = r#"
20//! -----BEGIN OPENSSH PRIVATE KEY-----
21//! b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBKH96ujW
22//! umB6/WnTNPjTeaAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN
23//! 796jTiQfZfG1KaT0PtFDJ/XFSqtiAAAAoFzvbvyFMhAiwBOXF0mhUUacPUCMZXivG2up2c
24//! hEnAw1b6BLRPyWbY5cC2n9ggD4ivJ1zSts6sBgjyiXQAReyrP35myYvT/OIB/NpwZM/xIJ
25//! N7MHSUzlkX4adBrga3f7GS4uv4ChOoxC4XsE5HsxtGsq1X8jzqLlZTmOcxkcEneYQexrUc
26//! bQP0o+gL5aKK8cQgiIlXeDbRjqhc4+h4EF6lY=
27//! -----END OPENSSH PRIVATE KEY-----
28//! "#;
29//!
30//! let encrypted_key = PrivateKey::from_openssh(encoded_key)?;
31//! assert!(encrypted_key.is_encrypted());
32//!
33//! // WARNING: don't hardcode passwords, and this one's bad anyway
34//! let password = "hunter42";
35//!
36//! let decrypted_key = encrypted_key.decrypt(password)?;
37//! assert!(!decrypted_key.is_encrypted());
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! ## Encrypting plaintext private keys
43//!
44//! When the `encryption` feature of this crate is enabled, it's possible to
45//! encrypt plaintext private keys under a provided password.
46//!
47//! The example below also requires enabling this crate's `getrandom` feature.
48//!
49#![cfg_attr(
50    all(feature = "ed25519", feature = "encryption", feature = "getrandom",),
51    doc = " ```"
52)]
53#![cfg_attr(
54    not(all(feature = "ed25519", feature = "encryption", feature = "getrandom",)),
55    doc = " ```ignore"
56)]
57//! # fn main() -> Result<(), ssh_key::Error> {
58//! use ssh_key::{Algorithm, PrivateKey, getrandom::SysRng, rand_core::UnwrapErr};
59//!
60//! // Generate a random key
61//! let mut rng = UnwrapErr(SysRng);
62//! let unencrypted_key = PrivateKey::random(&mut rng, Algorithm::Ed25519)?;
63//!
64//! // WARNING: don't hardcode passwords, and this one's bad anyway
65//! let password = "hunter42";
66//!
67//! let encrypted_key = unencrypted_key.encrypt(&mut SysRng, password)?;
68//! assert!(encrypted_key.is_encrypted());
69//! # Ok(())
70//! # }
71//! ```
72//!
73//! ## Generating random keys
74//!
75//! This crate supports generation of random keys using algorithm-specific
76//! backends gated on cargo features.
77//!
78//! The examples below require enabling this crate's `getrandom` feature as
79//! well as the crate feature identified in backticks in the title of each
80//! example.
81//!
82#![cfg_attr(all(feature = "ed25519", feature = "getrandom"), doc = " ```")]
83#![cfg_attr(
84    not(all(feature = "ed25519", feature = "getrandom")),
85    doc = " ```ignore"
86)]
87//! # fn main() -> Result<(), ssh_key::Error> {
88//! use ssh_key::{Algorithm, PrivateKey, getrandom::SysRng, rand_core::UnwrapErr};
89//!
90//! let mut rng = UnwrapErr(SysRng);
91//! let private_key = PrivateKey::random(&mut rng, Algorithm::Ed25519)?;
92//! # Ok(())
93//! # }
94//! ```
95
96#[cfg(feature = "alloc")]
97mod dsa;
98#[cfg(feature = "ecdsa")]
99mod ecdsa;
100mod ed25519;
101mod keypair;
102#[cfg(feature = "alloc")]
103mod opaque;
104#[cfg(feature = "alloc")]
105mod rsa;
106#[cfg(feature = "alloc")]
107mod sk;
108
109pub use self::{
110    ed25519::{Ed25519Keypair, Ed25519PrivateKey},
111    keypair::KeypairData,
112};
113
114#[cfg(feature = "alloc")]
115pub use crate::{
116    Comment, SshSig,
117    private::{
118        dsa::{DsaKeypair, DsaPrivateKey},
119        opaque::{OpaqueKeypair, OpaqueKeypairBytes, OpaquePrivateKeyBytes},
120        rsa::{RsaKeypair, RsaPrivateKey},
121        sk::SkEd25519,
122    },
123    sha2::Digest,
124};
125
126#[cfg(feature = "ecdsa")]
127pub use self::ecdsa::{EcdsaKeypair, EcdsaPrivateKey};
128
129#[cfg(all(feature = "alloc", feature = "ecdsa"))]
130pub use self::sk::SkEcdsaSha2NistP256;
131
132use crate::{Algorithm, Cipher, Error, Fingerprint, HashAlg, Kdf, PublicKey, Result, public};
133use cipher::Tag;
134use core::str;
135use ctutils::{Choice, CtEq};
136use encoding::{
137    CheckedSum, Decode, DecodePem, Encode, EncodePem, Reader, Writer,
138    pem::{LineEnding, PemLabel},
139};
140
141#[cfg(feature = "alloc")]
142use {
143    crate::AssociatedHashAlg,
144    alloc::{string::String, vec::Vec},
145    zeroize::Zeroizing,
146};
147
148#[cfg(feature = "encryption")]
149use rand_core::TryCryptoRng;
150
151#[cfg(feature = "rand_core")]
152use rand_core::CryptoRng;
153
154#[cfg(feature = "std")]
155use std::{fs::File, path::Path};
156
157#[cfg(feature = "std")]
158use std::io::{self, Read, Write};
159
160#[cfg(all(unix, feature = "std"))]
161use std::os::unix::fs::OpenOptionsExt;
162
163/// Error message for infallible conversions (used by `expect`)
164const CONVERSION_ERROR_MSG: &str = "SSH private key conversion error";
165
166/// Default key size to use for RSA keys in bits.
167#[cfg(all(feature = "rand_core", feature = "rsa"))]
168const DEFAULT_RSA_KEY_SIZE: usize = 4096;
169
170/// Maximum supported block size.
171///
172/// This is the block size used by e.g. AES.
173const MAX_BLOCK_SIZE: usize = 16;
174
175/// Padding bytes to use.
176const PADDING_BYTES: [u8; MAX_BLOCK_SIZE] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
177
178/// Unix file permissions for SSH private keys.
179#[cfg(all(unix, feature = "std"))]
180const UNIX_FILE_PERMISSIONS: u32 = 0o600;
181
182/// SSH private key.
183#[derive(Clone, Debug)]
184pub struct PrivateKey {
185    /// Cipher algorithm.
186    cipher: Cipher,
187
188    /// KDF options.
189    kdf: Kdf,
190
191    /// "Checkint" value used to verify successful decryption.
192    checkint: Option<u32>,
193
194    /// Public key.
195    public_key: PublicKey,
196
197    /// Private keypair data.
198    key_data: KeypairData,
199
200    /// Authentication tag for authenticated encryption modes.
201    auth_tag: Option<Tag>,
202}
203
204impl PrivateKey {
205    /// Magic string used to identify keys in this format.
206    const AUTH_MAGIC: &'static [u8] = b"openssh-key-v1\0";
207
208    /// Create a new unencrypted private key with the given keypair data and comment.
209    ///
210    /// On `no_alloc` platforms, use `PrivateKey::try_from(key_data)` instead.
211    ///
212    /// # Errors
213    /// Returns [`Error::Encrypted`] if the key is encrypted.
214    #[cfg(feature = "alloc")]
215    pub fn new(key_data: KeypairData, comment: impl Into<Comment>) -> Result<Self> {
216        if key_data.is_encrypted() {
217            return Err(Error::Encrypted);
218        }
219
220        let mut private_key = Self::try_from(key_data)?;
221        private_key.public_key.comment = comment.into();
222        Ok(private_key)
223    }
224
225    /// Parse an OpenSSH-formatted PEM private key.
226    ///
227    /// OpenSSH-formatted private keys begin with the following:
228    ///
229    /// ```text
230    /// -----BEGIN OPENSSH PRIVATE KEY-----
231    /// ```
232    ///
233    /// # Errors
234    /// Returns [`Error::Encoding`] in the event of an encoding error.
235    pub fn from_openssh(pem: impl AsRef<[u8]>) -> Result<Self> {
236        Self::decode_pem(pem)
237    }
238
239    /// Parse a PuTTY PPK private key.
240    ///
241    /// PPK-formatted private keys begin with the following:
242    ///
243    /// ```text
244    /// PuTTY-User-Key-File-<VERSION>: <ALGORITHM>
245    /// ```
246    ///
247    /// # Errors
248    /// Returns [`Error::Encoding`] in the event of an encoding error.
249    #[cfg(feature = "ppk")]
250    pub fn from_ppk(ppk: impl AsRef<str>, passphrase: Option<String>) -> Result<Self> {
251        use crate::ppk::PpkContainer;
252
253        let ppk: PpkContainer = PpkContainer::new(ppk.as_ref().try_into()?, passphrase)?;
254
255        Ok(Self {
256            auth_tag: None,
257            checkint: None,
258            cipher: Cipher::None,
259            kdf: Kdf::None,
260            key_data: ppk.keypair_data,
261            public_key: ppk.public_key,
262        })
263    }
264
265    /// Parse a raw binary SSH private key.
266    ///
267    /// # Errors
268    /// Returns [`Error::Encoding`] in the event of an encoding error.
269    pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
270        let reader = &mut bytes;
271        let private_key = Self::decode(reader)?;
272        Ok(reader.finish(private_key)?)
273    }
274
275    /// Encode OpenSSH-formatted (PEM) private key.
276    ///
277    /// # Errors
278    /// Returns [`Error::Encoding`] in the event of an encoding error.
279    pub fn encode_openssh<'o>(
280        &self,
281        line_ending: LineEnding,
282        out: &'o mut [u8],
283    ) -> Result<&'o str> {
284        Ok(self.encode_pem(line_ending, out)?)
285    }
286
287    /// Encode an OpenSSH-formatted PEM private key, allocating a self-zeroizing [`String`] for the
288    /// result.
289    ///
290    /// # Errors
291    /// Returns [`Error::Encoding`] in the event of an encoding error.
292    #[cfg(feature = "alloc")]
293    pub fn to_openssh(&self, line_ending: LineEnding) -> Result<Zeroizing<String>> {
294        Ok(self.encode_pem_string(line_ending).map(Zeroizing::new)?)
295    }
296
297    /// Serialize SSH private key as raw bytes.
298    ///
299    /// # Errors
300    /// Returns [`Error::Encoding`] in the event of an encoding error.
301    #[cfg(feature = "alloc")]
302    pub fn to_bytes(&self) -> Result<Zeroizing<Vec<u8>>> {
303        let mut private_key_bytes = Vec::with_capacity(self.encoded_len()?);
304        self.encode(&mut private_key_bytes)?;
305        Ok(Zeroizing::new(private_key_bytes))
306    }
307
308    /// Sign the given message using this private key, returning an [`SshSig`].
309    ///
310    /// These signatures can be produced using `ssh-keygen -Y sign`. They're
311    /// encoded as PEM and begin with the following:
312    ///
313    /// ```text
314    /// -----BEGIN SSH SIGNATURE-----
315    /// ```
316    ///
317    /// See [PROTOCOL.sshsig] for more information.
318    ///
319    /// # Usage
320    ///
321    /// See also: [`PublicKey::verify`].
322    ///
323    #[cfg_attr(feature = "ed25519", doc = "```")]
324    #[cfg_attr(not(feature = "ed25519"), doc = "```ignore")]
325    /// # fn main() -> Result<(), ssh_key::Error> {
326    /// use ssh_key::{PrivateKey, HashAlg, SshSig};
327    ///
328    /// // Message to be signed.
329    /// let message = b"testing";
330    ///
331    /// // Example domain/namespace used for the message.
332    /// let namespace = "example";
333    ///
334    /// // Private key to use when computing the signature.
335    /// // WARNING: don't actually hardcode private keys in source code!!!
336    /// let encoded_private_key = r#"
337    /// -----BEGIN OPENSSH PRIVATE KEY-----
338    /// b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
339    /// QyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYgAAAJgAIAxdACAM
340    /// XQAAAAtzc2gtZWQyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYg
341    /// AAAEC2BsIi0QwW2uFscKTUUXNHLsYX4FxlaSDSblbAj7WR7bM+rvN+ot98qgEN796jTiQf
342    /// ZfG1KaT0PtFDJ/XFSqtiAAAAEHVzZXJAZXhhbXBsZS5jb20BAgMEBQ==
343    /// -----END OPENSSH PRIVATE KEY-----
344    /// "#;
345    ///
346    /// let private_key = encoded_private_key.parse::<PrivateKey>()?;
347    /// let signature = private_key.sign(namespace, HashAlg::default(), message)?;
348    /// // assert!(private_key.public_key().verify(namespace, message, &signature).is_ok());
349    /// # Ok(())
350    /// # }
351    /// ```
352    ///
353    /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
354    ///
355    /// # Errors
356    /// Propagates errors from [`SshSig::sign`].
357    #[cfg(feature = "alloc")]
358    pub fn sign(&self, namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result<SshSig> {
359        SshSig::sign(self, namespace, hash_alg, msg)
360    }
361
362    /// Sign the given message [`Digest`] using this private key, returning an [`SshSig`].
363    ///
364    /// These signatures can be produced using `ssh-keygen -Y sign`.
365    ///
366    /// For more information, see [`PrivateKey::sign`].
367    ///
368    /// # Errors
369    /// Propagates errors from [`SshSig::sign_digest`].
370    #[cfg(feature = "alloc")]
371    pub fn sign_digest<D: AssociatedHashAlg + Digest>(
372        &self,
373        namespace: &str,
374        digest: D,
375    ) -> Result<SshSig> {
376        SshSig::sign_digest(self, namespace, digest)
377    }
378
379    /// Sign the given raw message prehash using this private key, returning an [`SshSig`].
380    ///
381    /// These signatures can be produced using `ssh-keygen -Y sign`.
382    ///
383    /// For more information, see [`PrivateKey::sign`].
384    ///
385    /// # Errors
386    /// Propagates errors from [`SshSig::sign_prehash`].
387    #[cfg(feature = "alloc")]
388    pub fn sign_prehash(
389        &self,
390        namespace: &str,
391        hash_alg: HashAlg,
392        prehash: &[u8],
393    ) -> Result<SshSig> {
394        SshSig::sign_prehash(self, namespace, hash_alg, prehash)
395    }
396
397    /// Read private key from an OpenSSH-formatted PEM source.
398    ///
399    /// # Errors
400    /// - Returns [`Error::Io`] on I/O errors.
401    /// - Returns [`Error::Encoding`] in the event of an encoding error.
402    #[cfg(feature = "std")]
403    pub fn read_openssh(reader: &mut impl Read) -> Result<Self> {
404        let pem = Zeroizing::new(io::read_to_string(reader)?);
405        Self::from_openssh(&*pem)
406    }
407
408    /// Read private key from an OpenSSH-formatted PEM file.
409    ///
410    /// # Errors
411    /// - Returns [`Error::Io`] on I/O errors.
412    /// - Returns [`Error::Encoding`] in the event of an encoding error.
413    #[cfg(feature = "std")]
414    pub fn read_openssh_file(path: impl AsRef<Path>) -> Result<Self> {
415        // TODO(tarcieri): verify file permissions match `UNIX_FILE_PERMISSIONS`
416        let mut file = File::open(path)?;
417        Self::read_openssh(&mut file)
418    }
419
420    /// Write private key as an OpenSSH-formatted PEM file.
421    ///
422    /// # Errors
423    /// - Returns [`Error::Io`] on I/O errors.
424    /// - Returns [`Error::Encoding`] in the event of an encoding error.
425    #[cfg(feature = "std")]
426    pub fn write_openssh(&self, writer: &mut impl Write, line_ending: LineEnding) -> Result<()> {
427        let pem = self.to_openssh(line_ending)?;
428        writer.write_all(pem.as_bytes())?;
429        Ok(())
430    }
431
432    /// Write private key as an OpenSSH-formatted PEM file.
433    ///
434    /// # Errors
435    /// - Returns [`Error::Io`] on I/O errors.
436    /// - Returns [`Error::Encoding`] in the event of an encoding error.
437    #[cfg(feature = "std")]
438    pub fn write_openssh_file(
439        &self,
440        path: impl AsRef<Path>,
441        line_ending: LineEnding,
442    ) -> Result<()> {
443        let mut options = File::options();
444
445        #[cfg(unix)]
446        options.mode(UNIX_FILE_PERMISSIONS);
447
448        let mut file = options.write(true).create(true).truncate(true).open(path)?;
449
450        self.write_openssh(&mut file, line_ending)
451    }
452
453    /// Attempt to decrypt an encrypted private key using the provided
454    /// password to derive an encryption key.
455    ///
456    /// # Errors
457    /// Returns [`Error::Decrypted`] if the private key is already decrypted.
458    #[cfg(feature = "encryption")]
459    pub fn decrypt(&self, password: impl AsRef<[u8]>) -> Result<Self> {
460        let (key, iv) = self.kdf.derive_key_and_iv(self.cipher, password)?;
461
462        let ciphertext = self.key_data.encrypted().ok_or(Error::Decrypted)?;
463        let mut buffer = Zeroizing::new(ciphertext.to_vec());
464        self.cipher.decrypt(&key, &iv, &mut buffer, self.auth_tag)?;
465
466        #[allow(clippy::arithmetic_side_effects)] // block sizes are constants
467        Self::decode_privatekey_comment_pair(
468            &mut &**buffer,
469            self.public_key.key_data.clone(),
470            self.cipher.block_size(),
471            self.cipher.block_size() - 1,
472        )
473    }
474
475    /// Encrypt an unencrypted private key using the provided password to
476    /// derive an encryption key.
477    ///
478    /// Uses the following algorithms:
479    /// - Cipher: [`Cipher::Aes256Ctr`]
480    /// - KDF: [`Kdf::Bcrypt`] (i.e. `bcrypt-pbkdf`)
481    ///
482    /// # Errors
483    /// Returns [`Error::Encrypted`] if the private key is already encrypted.
484    #[cfg(feature = "encryption")]
485    pub fn encrypt<R: TryCryptoRng + ?Sized>(
486        &self,
487        rng: &mut R,
488        password: impl AsRef<[u8]>,
489    ) -> Result<Self> {
490        self.encrypt_with_cipher(rng, Cipher::Aes256Ctr, password)
491    }
492
493    /// Encrypt an unencrypted private key using the provided password to
494    /// derive an encryption key for the provided [`Cipher`].
495    ///
496    /// # Errors
497    /// Returns [`Error::Encrypted`] if the private key is already encrypted.
498    #[cfg(feature = "encryption")]
499    pub fn encrypt_with_cipher<R: TryCryptoRng + ?Sized>(
500        &self,
501        rng: &mut R,
502        cipher: Cipher,
503        password: impl AsRef<[u8]>,
504    ) -> Result<Self> {
505        let checkint = rng.try_next_u32().map_err(|_| Error::RngFailure)?;
506
507        self.encrypt_with(
508            cipher,
509            Kdf::new(Default::default(), rng)?,
510            checkint,
511            password,
512        )
513    }
514
515    /// Encrypt an unencrypted private key using the provided cipher and KDF
516    /// configuration.
517    ///
518    /// # Errors
519    /// Returns [`Error::Encrypted`] if the private key is already encrypted.
520    #[cfg(feature = "encryption")]
521    pub fn encrypt_with(
522        &self,
523        cipher: Cipher,
524        kdf: Kdf,
525        checkint: u32,
526        password: impl AsRef<[u8]>,
527    ) -> Result<Self> {
528        if self.is_encrypted() {
529            return Err(Error::Encrypted);
530        }
531
532        let (key_bytes, iv_bytes) = kdf.derive_key_and_iv(cipher, password)?;
533        let msg_len = self.encoded_privatekey_comment_pair_len(cipher)?;
534        let mut out = Vec::with_capacity(msg_len);
535
536        // Encode and encrypt private key
537        self.encode_privatekey_comment_pair(&mut out, cipher, checkint)?;
538        let auth_tag = cipher.encrypt(&key_bytes, &iv_bytes, out.as_mut_slice())?;
539
540        Ok(Self {
541            cipher,
542            kdf,
543            checkint: None,
544            public_key: self.public_key.key_data.clone().into(),
545            key_data: KeypairData::Encrypted(out),
546            auth_tag,
547        })
548    }
549
550    /// Get the digital signature [`Algorithm`] used by this key.
551    #[must_use]
552    pub fn algorithm(&self) -> Algorithm {
553        self.public_key.algorithm()
554    }
555
556    /// Comment on the key (e.g. email address).
557    #[cfg(feature = "alloc")]
558    #[must_use]
559    pub fn comment(&self) -> &Comment {
560        self.public_key.comment()
561    }
562
563    /// Cipher algorithm (a.k.a. `ciphername`).
564    #[must_use]
565    pub fn cipher(&self) -> Cipher {
566        self.cipher
567    }
568
569    /// Compute key fingerprint.
570    ///
571    /// Use [`Default::default()`] to use the default hash function (SHA-256).
572    #[must_use]
573    pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint {
574        self.public_key.fingerprint(hash_alg)
575    }
576
577    /// Is this key encrypted?
578    #[must_use]
579    pub fn is_encrypted(&self) -> bool {
580        let ret = self.key_data.is_encrypted();
581        debug_assert_eq!(ret, self.cipher.is_some());
582        ret
583    }
584
585    /// Key Derivation Function (KDF) used to encrypt this key.
586    ///
587    /// Returns [`Kdf::None`] if this key is not encrypted.
588    #[must_use]
589    pub fn kdf(&self) -> &Kdf {
590        &self.kdf
591    }
592
593    /// Keypair data.
594    #[must_use]
595    pub fn key_data(&self) -> &KeypairData {
596        &self.key_data
597    }
598
599    /// Get the [`PublicKey`] which corresponds to this private key.
600    #[must_use]
601    pub fn public_key(&self) -> &PublicKey {
602        &self.public_key
603    }
604
605    /// Generate a random key which uses the given algorithm.
606    ///
607    /// # Errors
608    /// Returns `Error::AlgorithmUnknown` if the algorithm is unsupported.
609    #[cfg(feature = "rand_core")]
610    #[allow(unreachable_code, unused_variables)]
611    pub fn random<R: CryptoRng + ?Sized>(rng: &mut R, algorithm: Algorithm) -> Result<Self> {
612        let checkint = rng.next_u32();
613
614        let key_data = match algorithm {
615            #[cfg(feature = "dsa")]
616            Algorithm::Dsa => KeypairData::from(DsaKeypair::random(rng)?),
617            #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))]
618            Algorithm::Ecdsa { curve } => KeypairData::from(EcdsaKeypair::random(rng, curve)?),
619            #[cfg(feature = "ed25519")]
620            Algorithm::Ed25519 => KeypairData::from(Ed25519Keypair::random(rng)),
621            #[cfg(feature = "rsa")]
622            Algorithm::Rsa { .. } => {
623                KeypairData::from(RsaKeypair::random(rng, DEFAULT_RSA_KEY_SIZE)?)
624            }
625            _ => return Err(Error::AlgorithmUnknown),
626        };
627        let public_key = public::KeyData::try_from(&key_data)?;
628
629        Ok(Self {
630            cipher: Cipher::None,
631            kdf: Kdf::None,
632            checkint: Some(checkint),
633            public_key: public_key.into(),
634            key_data,
635            auth_tag: None,
636        })
637    }
638
639    /// Set the comment on the key.
640    #[cfg(feature = "alloc")]
641    pub fn set_comment(&mut self, comment: impl Into<Comment>) {
642        self.public_key.set_comment(comment);
643    }
644
645    /// Decode [`KeypairData`] along with its associated checkints and comment,
646    /// storing the comment in the provided public key on success.
647    ///
648    /// This method also checks padding for validity and ensures that the
649    /// decoded private key matches the provided public key.
650    ///
651    /// For private key format specification, see OpenSSH [PROTOCOL.key] ยง 3:
652    ///
653    /// ```text
654    /// uint32  checkint
655    /// uint32  checkint
656    /// byte[]  privatekey1
657    /// string  comment1
658    /// byte[]  privatekey2
659    /// string  comment2
660    /// ...
661    /// string  privatekeyN
662    /// string  commentN
663    /// char    1
664    /// char    2
665    /// char    3
666    /// ...
667    /// char    padlen % 255
668    /// ```
669    ///
670    /// [PROTOCOL.key]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
671    fn decode_privatekey_comment_pair(
672        reader: &mut impl Reader,
673        public_key: public::KeyData,
674        block_size: usize,
675        max_padding_size: usize,
676    ) -> Result<Self> {
677        debug_assert!(block_size <= MAX_BLOCK_SIZE);
678        debug_assert!(max_padding_size <= MAX_BLOCK_SIZE);
679
680        // Ensure input data is padding-aligned
681        if reader.remaining_len().checked_rem(block_size) != Some(0) {
682            return Err(encoding::Error::Length.into());
683        }
684
685        let checkint1 = u32::decode(reader)?;
686        let checkint2 = u32::decode(reader)?;
687
688        if checkint1 != checkint2 {
689            return Err(Error::Crypto);
690        }
691
692        let key_data = KeypairData::decode(reader)?;
693
694        // Ensure public key matches private key
695        if public_key != public::KeyData::try_from(&key_data)? {
696            return Err(Error::PublicKey);
697        }
698
699        let mut public_key = PublicKey::from(public_key);
700        public_key.decode_comment(reader)?;
701
702        let padding_len = reader.remaining_len();
703
704        if padding_len > max_padding_size {
705            return Err(encoding::Error::Length.into());
706        }
707
708        if padding_len != 0 {
709            let mut padding = [0u8; MAX_BLOCK_SIZE];
710            reader.read(&mut padding[..padding_len])?;
711
712            if PADDING_BYTES[..padding_len] != padding[..padding_len] {
713                return Err(Error::FormatEncoding);
714            }
715        }
716
717        if !reader.is_finished() {
718            return Err(Error::TrailingData {
719                remaining: reader.remaining_len(),
720            });
721        }
722
723        Ok(Self {
724            cipher: Cipher::None,
725            kdf: Kdf::None,
726            checkint: Some(checkint1),
727            public_key,
728            key_data,
729            auth_tag: None,
730        })
731    }
732
733    /// Encode [`KeypairData`] along with its associated checkints, comment,
734    /// and padding.
735    fn encode_privatekey_comment_pair(
736        &self,
737        writer: &mut impl Writer,
738        cipher: Cipher,
739        checkint: u32,
740    ) -> encoding::Result<()> {
741        let unpadded_len = self.unpadded_privatekey_comment_pair_len()?;
742        let padding_len = cipher.padding_len(unpadded_len);
743
744        checkint.encode(writer)?;
745        checkint.encode(writer)?;
746        self.key_data.encode(writer)?;
747
748        // Serialize comment
749        #[cfg(not(feature = "alloc"))]
750        b"".encode(writer)?;
751        #[cfg(feature = "alloc")]
752        self.comment().encode(writer)?;
753
754        writer.write(&PADDING_BYTES[..padding_len])?;
755        Ok(())
756    }
757
758    /// Get the length of this private key when encoded with the given comment
759    /// and padded using the padding size for the given cipher.
760    fn encoded_privatekey_comment_pair_len(&self, cipher: Cipher) -> encoding::Result<usize> {
761        let len = self.unpadded_privatekey_comment_pair_len()?;
762        [len, cipher.padding_len(len)].checked_sum()
763    }
764
765    /// Get the length of this private key when encoded with the given comment.
766    ///
767    /// This length is just the checkints, private key data, and comment sans
768    /// any padding.
769    fn unpadded_privatekey_comment_pair_len(&self) -> encoding::Result<usize> {
770        // This method is intended for use with unencrypted keys only
771        debug_assert!(!self.is_encrypted(), "called on encrypted key");
772
773        #[cfg(not(feature = "alloc"))]
774        let comment_len = 0;
775        #[cfg(feature = "alloc")]
776        let comment_len = self.comment().encoded_len()?;
777
778        [
779            8, // 2 x uint32 checkints,
780            self.key_data.encoded_len()?,
781            comment_len,
782        ]
783        .checked_sum()
784    }
785}
786
787impl CtEq for PrivateKey {
788    fn ct_eq(&self, other: &Self) -> Choice {
789        // Constant-time with respect to private key data
790        self.key_data.ct_eq(&other.key_data)
791            & Choice::from(u8::from(
792                self.cipher == other.cipher
793                    && self.kdf == other.kdf
794                    && self.public_key == other.public_key,
795            ))
796    }
797}
798
799impl Eq for PrivateKey {}
800
801impl PartialEq for PrivateKey {
802    fn eq(&self, other: &Self) -> bool {
803        self.ct_eq(other).into()
804    }
805}
806
807impl Decode for PrivateKey {
808    type Error = Error;
809
810    fn decode(reader: &mut impl Reader) -> Result<Self> {
811        let mut auth_magic = [0u8; Self::AUTH_MAGIC.len()];
812        reader.read(&mut auth_magic)?;
813
814        if auth_magic != Self::AUTH_MAGIC {
815            return Err(Error::FormatEncoding);
816        }
817
818        let cipher = Cipher::decode(reader)?;
819        let kdf = Kdf::decode(reader)?;
820        let nkeys = usize::decode(reader)?;
821
822        // TODO(tarcieri): support more than one key?
823        if nkeys != 1 {
824            return Err(encoding::Error::Length.into());
825        }
826
827        let public_key = reader.read_prefixed(public::KeyData::decode)?;
828
829        // Handle encrypted private key
830        #[cfg(not(feature = "alloc"))]
831        if cipher.is_some() {
832            return Err(Error::Encrypted);
833        }
834        #[cfg(feature = "alloc")]
835        if cipher.is_some() {
836            let ciphertext = Vec::decode(reader)?;
837
838            // Ensure ciphertext is padded to the expected length
839            if ciphertext.len().checked_rem(cipher.block_size()) != Some(0) {
840                return Err(Error::Crypto);
841            }
842
843            let auth_tag = if cipher.has_tag() {
844                let mut tag = Tag::default();
845                reader.read(&mut tag)?;
846                Some(tag)
847            } else {
848                None
849            };
850
851            if !reader.is_finished() {
852                return Err(Error::TrailingData {
853                    remaining: reader.remaining_len(),
854                });
855            }
856
857            return Ok(Self {
858                cipher,
859                kdf,
860                checkint: None,
861                public_key: public_key.into(),
862                key_data: KeypairData::Encrypted(ciphertext),
863                auth_tag,
864            });
865        }
866
867        // Processing unencrypted key. No KDF should be set.
868        if kdf.is_some() {
869            return Err(Error::Crypto);
870        }
871
872        reader.read_prefixed(|reader| {
873            // PuTTYgen uses a non-standard block size of 16
874            // and _always_ adds a padding even if data length
875            // is divisible by 16 - for unencrypted keys
876            // in the OpenSSH format.
877            // We're only relaxing the exact length check, but will
878            // still validate that the contents of the padding area.
879            // In all other cases there can be up to (but not including)
880            // `block_size` padding bytes as per `PROTOCOL.key`.
881            let max_padding_size = match cipher {
882                Cipher::None => 16,
883                #[allow(clippy::arithmetic_side_effects)] // block sizes are constants
884                _ => cipher.block_size() - 1,
885            };
886            Self::decode_privatekey_comment_pair(
887                reader,
888                public_key,
889                cipher.block_size(),
890                max_padding_size,
891            )
892        })
893    }
894}
895
896impl Encode for PrivateKey {
897    fn encoded_len(&self) -> encoding::Result<usize> {
898        let private_key_len = if self.is_encrypted() {
899            self.key_data.encoded_len_prefixed()?
900        } else {
901            [4, self.encoded_privatekey_comment_pair_len(Cipher::None)?].checked_sum()?
902        };
903
904        [
905            Self::AUTH_MAGIC.len(),
906            self.cipher.encoded_len()?,
907            self.kdf.encoded_len()?,
908            4, // number of keys (uint32)
909            self.public_key.key_data().encoded_len_prefixed()?,
910            private_key_len,
911            self.auth_tag.map_or(0, |tag| tag.len()),
912        ]
913        .checked_sum()
914    }
915
916    fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
917        writer.write(Self::AUTH_MAGIC)?;
918        self.cipher.encode(writer)?;
919        self.kdf.encode(writer)?;
920
921        // TODO(tarcieri): support for encoding more than one private key
922        1usize.encode(writer)?;
923
924        // Encode public key
925        self.public_key.key_data().encode_prefixed(writer)?;
926
927        // Encode private key
928        if self.is_encrypted() {
929            self.key_data.encode_prefixed(writer)?;
930
931            if let Some(tag) = &self.auth_tag {
932                writer.write(tag)?;
933            }
934        } else {
935            self.encoded_privatekey_comment_pair_len(Cipher::None)?
936                .encode(writer)?;
937
938            let checkint = self.checkint.unwrap_or_else(|| self.key_data.checkint());
939            self.encode_privatekey_comment_pair(writer, Cipher::None, checkint)?;
940        }
941
942        Ok(())
943    }
944}
945
946impl From<PrivateKey> for PublicKey {
947    fn from(private_key: PrivateKey) -> PublicKey {
948        private_key.public_key
949    }
950}
951
952impl From<&PrivateKey> for PublicKey {
953    fn from(private_key: &PrivateKey) -> PublicKey {
954        private_key.public_key.clone()
955    }
956}
957
958impl From<PrivateKey> for public::KeyData {
959    fn from(private_key: PrivateKey) -> public::KeyData {
960        private_key.public_key.key_data
961    }
962}
963
964impl From<&PrivateKey> for public::KeyData {
965    fn from(private_key: &PrivateKey) -> public::KeyData {
966        private_key.public_key.key_data.clone()
967    }
968}
969
970#[cfg(feature = "alloc")]
971impl From<DsaKeypair> for PrivateKey {
972    fn from(keypair: DsaKeypair) -> PrivateKey {
973        KeypairData::from(keypair)
974            .try_into()
975            .expect(CONVERSION_ERROR_MSG)
976    }
977}
978
979#[cfg(feature = "ecdsa")]
980impl From<EcdsaKeypair> for PrivateKey {
981    fn from(keypair: EcdsaKeypair) -> PrivateKey {
982        KeypairData::from(keypair)
983            .try_into()
984            .expect(CONVERSION_ERROR_MSG)
985    }
986}
987
988impl From<Ed25519Keypair> for PrivateKey {
989    fn from(keypair: Ed25519Keypair) -> PrivateKey {
990        KeypairData::from(keypair)
991            .try_into()
992            .expect(CONVERSION_ERROR_MSG)
993    }
994}
995
996#[cfg(feature = "alloc")]
997impl From<RsaKeypair> for PrivateKey {
998    fn from(keypair: RsaKeypair) -> PrivateKey {
999        KeypairData::from(keypair)
1000            .try_into()
1001            .expect(CONVERSION_ERROR_MSG)
1002    }
1003}
1004
1005#[cfg(all(feature = "alloc", feature = "ecdsa"))]
1006impl From<SkEcdsaSha2NistP256> for PrivateKey {
1007    fn from(keypair: SkEcdsaSha2NistP256) -> PrivateKey {
1008        KeypairData::from(keypair)
1009            .try_into()
1010            .expect(CONVERSION_ERROR_MSG)
1011    }
1012}
1013
1014#[cfg(feature = "alloc")]
1015impl From<SkEd25519> for PrivateKey {
1016    fn from(keypair: SkEd25519) -> PrivateKey {
1017        KeypairData::from(keypair)
1018            .try_into()
1019            .expect(CONVERSION_ERROR_MSG)
1020    }
1021}
1022
1023impl TryFrom<KeypairData> for PrivateKey {
1024    type Error = Error;
1025
1026    fn try_from(key_data: KeypairData) -> Result<PrivateKey> {
1027        let public_key = public::KeyData::try_from(&key_data)?;
1028
1029        Ok(Self {
1030            cipher: Cipher::None,
1031            kdf: Kdf::None,
1032            checkint: None,
1033            public_key: public_key.into(),
1034            key_data,
1035            auth_tag: None,
1036        })
1037    }
1038}
1039
1040impl PemLabel for PrivateKey {
1041    const PEM_LABEL: &'static str = "OPENSSH PRIVATE KEY";
1042}
1043
1044impl str::FromStr for PrivateKey {
1045    type Err = Error;
1046
1047    fn from_str(s: &str) -> Result<Self> {
1048        Self::from_openssh(s)
1049    }
1050}