Skip to main content

bitwarden_ssh/
export.rs

1use pkcs8::EncodePrivateKey;
2use rsa::RsaPrivateKey;
3use ssh_key::{EcdsaCurve, PrivateKey};
4
5use crate::error::SshKeyExportError;
6
7/// Convert an OpenSSH private key to PKCS#8 DER format
8///
9/// This is primarily used for exporting SSH keys to other credential managers using Credential
10/// Exchange.
11pub fn export_pkcs8_der_key(private_key: &str) -> Result<Vec<u8>, SshKeyExportError> {
12    // Parse the OpenSSH private key
13    let private_key =
14        PrivateKey::from_openssh(private_key).map_err(|_| SshKeyExportError::KeyConversion)?;
15
16    match private_key.key_data() {
17        ssh_key::private::KeypairData::Ed25519(keypair) => {
18            let sk: ed25519_dalek::SigningKey = keypair
19                .try_into()
20                .map_err(|_| SshKeyExportError::KeyConversion)?;
21
22            Ok(sk
23                .to_pkcs8_der()
24                .map_err(|_| SshKeyExportError::KeyConversion)?
25                .as_bytes()
26                .to_vec())
27        }
28        ssh_key::private::KeypairData::Rsa(keypair) => {
29            let rk: RsaPrivateKey = keypair
30                .try_into()
31                .map_err(|_| SshKeyExportError::KeyConversion)?;
32
33            Ok(rk
34                .to_pkcs8_der()
35                .map_err(|_| SshKeyExportError::KeyConversion)?
36                .as_bytes()
37                .to_vec())
38        }
39        ssh_key::private::KeypairData::Ecdsa(keypair) => export_ecdsa_pkcs8_der(keypair),
40        _ => Err(SshKeyExportError::KeyConversion),
41    }
42}
43
44fn export_ecdsa_pkcs8_der(
45    keypair: &ssh_key::private::EcdsaKeypair,
46) -> Result<Vec<u8>, SshKeyExportError> {
47    let curve = keypair.curve();
48    let private_key_bytes = keypair.private_key_bytes();
49
50    match curve {
51        EcdsaCurve::NistP256 => {
52            let sk = p256::SecretKey::from_slice(private_key_bytes)
53                .map_err(|_| SshKeyExportError::KeyConversion)?;
54            Ok(sk
55                .to_pkcs8_der()
56                .map_err(|_| SshKeyExportError::KeyConversion)?
57                .as_bytes()
58                .to_vec())
59        }
60        EcdsaCurve::NistP384 => {
61            let sk = p384::SecretKey::from_slice(private_key_bytes)
62                .map_err(|_| SshKeyExportError::KeyConversion)?;
63            Ok(sk
64                .to_pkcs8_der()
65                .map_err(|_| SshKeyExportError::KeyConversion)?
66                .as_bytes()
67                .to_vec())
68        }
69        EcdsaCurve::NistP521 => {
70            let sk = p521::SecretKey::from_slice(private_key_bytes)
71                .map_err(|_| SshKeyExportError::KeyConversion)?;
72            Ok(sk
73                .to_pkcs8_der()
74                .map_err(|_| SshKeyExportError::KeyConversion)?
75                .as_bytes()
76                .to_vec())
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::import::{import_key, import_pkcs8_der_key};
85
86    #[test]
87    fn export_ed25519_openssh_unencrypted() {
88        let private_key = include_str!("../resources/import/ed25519_openssh_unencrypted");
89        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
90
91        let exported_key = export_pkcs8_der_key(&result.private_key).unwrap();
92        let expected_pkcs8_der: Vec<u8> = vec![
93            48, 81, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32, 139, 118, 81, 75, 32, 150,
94            196, 136, 90, 63, 127, 68, 78, 117, 115, 13, 100, 3, 199, 24, 243, 97, 189, 182, 223,
95            181, 163, 236, 81, 145, 35, 104, 129, 33, 0, 50, 66, 141, 182, 77, 117, 205, 170, 241,
96            126, 47, 200, 212, 73, 35, 94, 187, 197, 42, 174, 192, 227, 189, 255, 105, 192, 140, 3,
97            11, 211, 11, 234,
98        ];
99        assert_eq!(exported_key, expected_pkcs8_der);
100
101        // Confirm the public key of the re-imported key is the same ignoring the key comment
102        let reimported_key = import_pkcs8_der_key(&exported_key).unwrap();
103        assert_eq!(
104            reimported_key.public_key,
105            result.public_key.strip_suffix(" testkey").unwrap()
106        );
107    }
108
109    #[test]
110    fn export_rsa_openssh_unencrypted() {
111        let private_key = include_str!("../resources/import/rsa_openssh_unencrypted");
112        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
113
114        let exported_key = export_pkcs8_der_key(&result.private_key).unwrap();
115
116        // Confirm the public key of the re-imported key is the same ignoring the key comment
117        let reimported_key = import_pkcs8_der_key(&exported_key).unwrap();
118        assert_eq!(
119            reimported_key.public_key,
120            result.public_key.strip_suffix(" testkey").unwrap()
121        );
122    }
123
124    #[test]
125    fn export_ecdsa_p256() {
126        let private_key = include_str!("../resources/generator/ecdsa_p256_key");
127        let exported_key = export_pkcs8_der_key(private_key).unwrap();
128        // Verify the PKCS8 DER is non-empty and re-importable
129        assert!(!exported_key.is_empty());
130    }
131
132    #[test]
133    fn export_ecdsa_p384() {
134        let private_key = include_str!("../resources/generator/ecdsa_p384_key");
135        let exported_key = export_pkcs8_der_key(private_key).unwrap();
136        assert!(!exported_key.is_empty());
137    }
138
139    #[test]
140    fn export_ecdsa_p521() {
141        let private_key = include_str!("../resources/generator/ecdsa_p521_key");
142        let exported_key = export_pkcs8_der_key(private_key).unwrap();
143        assert!(!exported_key.is_empty());
144    }
145
146    #[cfg(feature = "ecdsa-keys")]
147    #[test]
148    fn export_ecdsa_p256_roundtrip() {
149        let private_key = include_str!("../resources/generator/ecdsa_p256_key");
150        let view = import_key(private_key.to_string(), None).unwrap();
151
152        let exported_key = export_pkcs8_der_key(&view.private_key).unwrap();
153        let reimported = import_pkcs8_der_key(&exported_key).unwrap();
154        assert_eq!(reimported.public_key, view.public_key);
155    }
156
157    #[cfg(feature = "ecdsa-keys")]
158    #[test]
159    fn export_ecdsa_p384_roundtrip() {
160        let private_key = include_str!("../resources/generator/ecdsa_p384_key");
161        let view = import_key(private_key.to_string(), None).unwrap();
162
163        let exported_key = export_pkcs8_der_key(&view.private_key).unwrap();
164        let reimported = import_pkcs8_der_key(&exported_key).unwrap();
165        assert_eq!(reimported.public_key, view.public_key);
166    }
167
168    #[cfg(feature = "ecdsa-keys")]
169    #[test]
170    fn export_ecdsa_p521_roundtrip() {
171        let private_key = include_str!("../resources/generator/ecdsa_p521_key");
172        let view = import_key(private_key.to_string(), None).unwrap();
173
174        let exported_key = export_pkcs8_der_key(&view.private_key).unwrap();
175        let reimported = import_pkcs8_der_key(&exported_key).unwrap();
176        assert_eq!(reimported.public_key, view.public_key);
177    }
178}