1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 19:23:19 +00:00

SSH Agent v2: Add ssh key primitive types (#18583)

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
This commit is contained in:
neuronull
2026-02-04 13:01:18 -08:00
committed by GitHub
parent afc46cc50a
commit 34108d93e4
6 changed files with 227 additions and 4 deletions

4
.github/CODEOWNERS vendored
View File

@@ -156,6 +156,8 @@ apps/desktop/macos/autofill-extension @bitwarden/team-autofill-desktop-dev
apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-desktop-dev
apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofill-desktop-dev
apps/desktop/desktop_native/autotype @bitwarden/team-autofill-desktop-dev
apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys
apps/desktop/desktop_native/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys
apps/desktop/desktop_native/napi/src/autofill.rs @bitwarden/team-autofill-desktop-dev
apps/desktop/desktop_native/napi/src/autotype.rs @bitwarden/team-autofill-desktop-dev
apps/desktop/desktop_native/napi/src/sshagent.rs @bitwarden/team-autofill-desktop-dev
@@ -164,8 +166,6 @@ apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-desktop-dev
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-desktop-dev
apps/desktop/src/services/encrypted-message-handler.service.ts @bitwarden/team-autofill-desktop-dev
.github/workflows/alert-ddg-files-modified.yml @bitwarden/team-autofill-desktop-dev
# SSH Agent
apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys
## UI Foundation ##
.github/workflows/chromatic.yml @bitwarden/team-ui-foundation

View File

@@ -377,9 +377,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.7.3"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "basic-toml"
@@ -3295,6 +3295,9 @@ dependencies = [
"bcrypt-pbkdf",
"ed25519-dalek",
"num-bigint-dig",
"p256",
"p384",
"p521",
"rand_core 0.6.4",
"rsa",
"sec1",
@@ -3306,6 +3309,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "ssh_agent"
version = "0.0.0"
dependencies = [
"anyhow",
"base64",
"ssh-key",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"

View File

@@ -9,6 +9,7 @@ members = [
"napi",
"process_isolation",
"proxy",
"ssh_agent",
"windows_plugin_authenticator",
]

View File

@@ -0,0 +1,19 @@
[package]
name = "ssh_agent"
edition = { workspace = true }
license = { workspace = true }
version = { workspace = true }
publish = { workspace = true }
[dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }
ssh-key = { version = "=0.6.7", features = [
"encryption",
"ed25519",
"rsa",
"rand_core",
] }
[lints]
workspace = true

View File

@@ -0,0 +1,184 @@
//! Cryptographic key management for the SSH agent.
//!
//! This module provides the core primitive types and functionality for managing
//! SSH keys in the Bitwarden SSH agent.
//!
//! # Supported signing algorithms
//!
//! - Ed25519
//! - RSA
//!
//! ECDSA keys are not currently supported (PM-29894)
use std::fmt;
use anyhow::anyhow;
use ssh_key::private::{Ed25519Keypair, RsaKeypair};
/// Represents an SSH key and its associated metadata.
#[derive(Clone)]
pub(crate) struct SSHKeyData {
/// Private key of the key pair
private_key: PrivateKey,
/// Public key of the key pair
public_key: PublicKey,
/// Human-readable name
name: String,
/// Vault cipher ID associated with the key pair
cipher_id: String,
}
impl SSHKeyData {
/// Creates a new `SSHKeyData` instance.
///
/// # Arguments
///
/// * `private_key` - The private key component
/// * `public_key` - The public key component
/// * `name` - A human-readable name for the key
/// * `cipher_id` - The vault cipher identifier associated with this key
pub(crate) fn new(
private_key: PrivateKey,
public_key: PublicKey,
name: String,
cipher_id: String,
) -> Self {
Self {
private_key,
public_key,
name,
cipher_id,
}
}
/// # Returns
///
/// A reference to the [`PublicKey`].
pub(crate) fn public_key(&self) -> &PublicKey {
&self.public_key
}
/// # Returns
///
/// A reference to the [`PrivateKey`].
pub(crate) fn private_key(&self) -> &PrivateKey {
&self.private_key
}
/// # Returns
///
/// A reference to the human-readable name for this key.
pub(crate) fn name(&self) -> &String {
&self.name
}
/// # Returns
///
/// A reference to the cipher ID that links this key to a vault entry.
pub(crate) fn cipher_id(&self) -> &String {
&self.cipher_id
}
}
/// Represents an SSH private key.
#[derive(Clone, PartialEq, Debug)]
pub(crate) enum PrivateKey {
Ed25519(Ed25519Keypair),
Rsa(RsaKeypair),
}
impl TryFrom<ssh_key::private::PrivateKey> for PrivateKey {
type Error = anyhow::Error;
fn try_from(key: ssh_key::private::PrivateKey) -> Result<Self, Self::Error> {
match key.algorithm() {
ssh_key::Algorithm::Ed25519 => Ok(Self::Ed25519(
key.key_data()
.ed25519()
.ok_or(anyhow!("Failed to parse ed25519 key"))?
.to_owned(),
)),
ssh_key::Algorithm::Rsa { hash: _ } => Ok(Self::Rsa(
key.key_data()
.rsa()
.ok_or(anyhow!("Failed to parse RSA key"))?
.to_owned(),
)),
_ => Err(anyhow!("Unsupported key type")),
}
}
}
/// Represents an SSH public key.
///
/// Contains the algorithm identifier (e.g., "ssh-ed25519", "ssh-rsa")
/// and the binary blob of the public key data.
#[derive(Clone, Ord, Eq, PartialOrd, PartialEq)]
pub(crate) struct PublicKey {
pub alg: String,
pub blob: Vec<u8>,
}
impl PublicKey {
pub(crate) fn alg(&self) -> &str {
&self.alg
}
pub(crate) fn blob(&self) -> &[u8] {
&self.blob
}
}
impl fmt::Debug for PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PublicKey(\"{self}\")")
}
}
impl fmt::Display for PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use base64::{prelude::BASE64_STANDARD, Engine as _};
write!(f, "{} {}", self.alg(), BASE64_STANDARD.encode(self.blob()))
}
}
#[cfg(test)]
mod tests {
use ssh_key::{
private::{Ed25519Keypair, RsaKeypair},
rand_core::OsRng,
LineEnding,
};
use super::*;
const MIN_KEY_BIT_SIZE: usize = 2048;
fn create_valid_ed25519_key_string() -> String {
let ed25519_keypair = Ed25519Keypair::random(&mut OsRng);
let ssh_key =
ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ed25519(ed25519_keypair), "")
.unwrap();
ssh_key.to_openssh(LineEnding::LF).unwrap().to_string()
}
#[test]
fn test_privatekey_from_ed25519() {
let key_string = create_valid_ed25519_key_string();
let ssh_key = ssh_key::PrivateKey::from_openssh(&key_string).unwrap();
let private_key = PrivateKey::try_from(ssh_key).unwrap();
assert!(matches!(private_key, PrivateKey::Ed25519(_)));
}
#[test]
fn test_privatekey_from_rsa() {
let rsa_keypair = RsaKeypair::random(&mut OsRng, MIN_KEY_BIT_SIZE).unwrap();
let ssh_key =
ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Rsa(rsa_keypair), "").unwrap();
let private_key = PrivateKey::try_from(ssh_key).unwrap();
assert!(matches!(private_key, PrivateKey::Rsa(_)));
}
}

View File

@@ -0,0 +1,7 @@
//! Bitwarden SSH Agent implementation
//!
//! <https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#RFC4253>
#![allow(dead_code)] // TODO remove when all code is used in follow-up PR
mod crypto;