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