diff --git a/Cargo.lock b/Cargo.lock index f979b7aa..b8451af1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -686,6 +686,32 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "cypheraddr" version = "0.4.0" @@ -843,7 +869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdfd533a2fc01178c738c99412ae1f7e1ad2cb37c2e14bfd87e9d4618171c825" dependencies = [ "ct-codecs", - "ed25519", + "ed25519 1.5.3", "getrandom 0.2.17", ] @@ -870,6 +896,27 @@ dependencies = [ "signature 1.6.4", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519 2.2.3", + "sha2", + "subtle", +] + [[package]] name = "either" version = "1.15.0" @@ -1021,6 +1068,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.27" @@ -2953,7 +3006,6 @@ dependencies = [ "radicle-git-ref-format", "radicle-localtime", "radicle-oid", - "radicle-ssh", "schemars", "serde", "serde-untagged", @@ -3083,14 +3135,15 @@ dependencies = [ "multibase", "qcheck", "qcheck-macros", - "radicle-ssh", "schemars", "serde", "signature 2.2.0", "sqlite", + "ssh-agent-lib", "ssh-key", "tempfile", "thiserror 2.0.18", + "winpipe", "zeroize", ] @@ -3270,15 +3323,6 @@ dependencies = [ "windows 0.62.2", ] -[[package]] -name = "radicle-ssh" -version = "0.10.0" -dependencies = [ - "thiserror 2.0.18", - "winpipe", - "zeroize", -] - [[package]] name = "radicle-std-ext" version = "0.2.0" @@ -3648,6 +3692,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "sem_safe" version = "0.2.1" @@ -3989,6 +4042,22 @@ dependencies = [ "sqlite3-src", ] +[[package]] +name = "ssh-agent-lib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0582e6e5724c4a5d038472976e8fad64d824b3de37210aa60c35883e3e766" +dependencies = [ + "byteorder", + "log", + "secrecy", + "signature 2.2.0", + "ssh-encoding", + "ssh-key", + "subtle", + "thiserror 2.0.18", +] + [[package]] name = "ssh-cipher" version = "0.2.0" @@ -4024,6 +4093,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" dependencies = [ "bcrypt-pbkdf", + "ed25519-dalek", + "num-bigint-dig", "p256", "p384", "p521", diff --git a/Cargo.toml b/Cargo.toml index 418d26fe..16e3773e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,6 @@ radicle-node = { version = "0.19", path = "crates/radicle-node" } radicle-oid = { version = "0.1.0", path = "crates/radicle-oid", default-features = false } radicle-protocol = { version = "0.7", path = "crates/radicle-protocol" } radicle-signals = { version = "0.11", path = "crates/radicle-signals" } -radicle-ssh = { version = "0.10", path = "crates/radicle-ssh", default-features = false } radicle-systemd = { version = "0.12", path = "crates/radicle-systemd" } radicle-term = { version = "0.17", path = "crates/radicle-term" } radicle-windows = { version = "0.1", path = "crates/radicle-windows" } diff --git a/HACKING.md b/HACKING.md index e900fcf9..a587c0c4 100644 --- a/HACKING.md +++ b/HACKING.md @@ -26,7 +26,6 @@ The repository is structured in *crates*, as follows: * `radicle-dag`: A simple directed acyclic graph implementation used by `radicle-cob`. * `radicle-node`: The radicle peer-to-peer daemon that enables users to connect to the network and share code. * `radicle-remote-helper`: A Git remote helper for `rad://` remotes. -* `radicle-ssh`: OpenSSH functionality, including a library used to interface with `ssh-agent`. * `radicle-term`: A generic terminal library used by the Radicle CLI. * `radicle-tools`: Tools used to aid in the development of Radicle. diff --git a/crates/radicle-cli/src/commands/self.rs b/crates/radicle-cli/src/commands/self.rs index 1feb12f5..ec3d3c2a 100644 --- a/crates/radicle-cli/src/commands/self.rs +++ b/crates/radicle-cli/src/commands/self.rs @@ -63,12 +63,7 @@ fn all(profile: &Profile) -> anyhow::Result<()> { table.push([term::format::style("Node").into(), node.to_string().into()]); let ssh_agent = match ssh::agent::Agent::connect() { - Ok(c) => term::format::positive(format!( - "running ({})", - c.path() - .map(|p| p.display().to_string()) - .unwrap_or(String::from("?")) - )), + Ok(c) => term::format::positive(format!("running ({})", c.path().display())), Err(e) if e.is_not_running() => term::format::yellow(String::from("not running")), Err(e) => term::format::negative(format!("error: {e}")), }; diff --git a/crates/radicle-crypto/Cargo.toml b/crates/radicle-crypto/Cargo.toml index 1450f488..0dff49ee 100644 --- a/crates/radicle-crypto/Cargo.toml +++ b/crates/radicle-crypto/Cargo.toml @@ -14,7 +14,7 @@ rust-version.workspace = true [features] test = ["fastrand", "qcheck"] -ssh = ["radicle-ssh", "ssh-key"] +ssh = ["ssh-agent-lib", "ssh-key"] [dependencies] amplify = { workspace = true } @@ -24,15 +24,18 @@ fastrand = { workspace = true, optional = true } multibase = { workspace = true } qcheck = { workspace = true, optional = true } git-ref-format-core = { workspace = true, optional = true } -radicle-ssh = { workspace = true, optional = true } schemars = { workspace = true, optional = true, features = ["derive", "std"] } serde = { workspace = true, features = ["derive", "std"] } signature = { workspace = true, features = ["std"] } sqlite = { workspace = true, features = ["bundled"], optional = true } +ssh-agent-lib = { version = "0.5.2", optional = true, default-features = false, features = ["log"] } ssh-key = { version = "0.6.3", default-features = false, features = ["std", "encryption", "getrandom"], optional = true } thiserror = { workspace = true, default-features = true } zeroize = { workspace = true } +[target.'cfg(windows)'.dependencies] +winpipe = { workspace = true } + [dev-dependencies] fastrand = { workspace = true } qcheck = { workspace = true } diff --git a/crates/radicle-crypto/src/ssh.rs b/crates/radicle-crypto/src/ssh.rs index 45259ca5..9c1516bd 100644 --- a/crates/radicle-crypto/src/ssh.rs +++ b/crates/radicle-crypto/src/ssh.rs @@ -1,17 +1,9 @@ pub mod agent; pub mod keystore; -use std::io; - use thiserror::Error; -use radicle_ssh::encoding; -use radicle_ssh::encoding::Encodable; -use radicle_ssh::encoding::Encoding; -use radicle_ssh::encoding::Reader; - use crate as crypto; -use crate::PublicKey; pub use keystore::{Keystore, Passphrase}; @@ -127,218 +119,3 @@ pub mod fmt { } } } - -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum SignatureError { - #[error(transparent)] - Invalid(#[from] crypto::Error), - #[error(transparent)] - Encoding(#[from] encoding::Error), - #[error("unknown algorithm '{0}'")] - UnknownAlgorithm(String), -} - -impl Encodable for crypto::Signature { - type Error = SignatureError; - - fn read(r: &mut encoding::Cursor) -> Result { - let buf = r.read_string()?; - let mut inner_strs = buf.reader(0); - - let sig_type = inner_strs.read_string()?; - if sig_type != b"ssh-ed25519" { - return Err(SignatureError::UnknownAlgorithm( - String::from_utf8_lossy(sig_type).to_string(), - )); - } - let sig = crypto::Signature::try_from(inner_strs.read_string()?)?; - - Ok(sig) - } - - fn write(&self, buf: &mut E) { - let mut inner_strs = Vec::new(); - inner_strs.extend_ssh_string(b"ssh-ed25519"); - inner_strs.extend_ssh_string(self.as_ref()); - buf.extend_ssh_string(&inner_strs); - } -} - -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum PublicKeyError { - #[error(transparent)] - Invalid(#[from] crypto::Error), - #[error(transparent)] - Encoding(#[from] encoding::Error), - #[error("unknown algorithm '{0}'")] - UnknownAlgorithm(String), -} - -impl Encodable for PublicKey { - type Error = PublicKeyError; - - fn read(r: &mut encoding::Cursor) -> Result { - match r.read_string()? { - b"ssh-ed25519" => { - let s = r.read_string()?; - let p = PublicKey::try_from(s)?; - - Ok(p) - } - v => Err(PublicKeyError::UnknownAlgorithm( - String::from_utf8_lossy(v).to_string(), - )), - } - } - - fn write(&self, w: &mut E) { - let mut str_w: Vec = Vec::::new(); - str_w.extend_ssh_string(b"ssh-ed25519"); - str_w.extend_ssh_string(&self[..]); - w.extend_ssh_string(&str_w) - } -} - -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum SecretKeyError { - #[error(transparent)] - Encoding(#[from] encoding::Error), - #[error(transparent)] - Crypto(#[from] crypto::Error), - #[error(transparent)] - Io(#[from] io::Error), - #[error("unknown algorithm '{0}'")] - UnknownAlgorithm(String), - #[error("public key does not match secret key")] - Mismatch, -} - -impl Encodable for crypto::SecretKey { - type Error = SecretKeyError; - - fn read(r: &mut encoding::Cursor) -> Result { - match r.read_string()? { - b"ssh-ed25519" => { - let public = r.read_string()?; - let pair = r.read_string()?; - let _comment = r.read_string()?; - let key = crypto::SecretKey::try_from(pair)?; - - if public != key.public_key().as_ref() { - return Err(SecretKeyError::Mismatch); - } - Ok(key) - } - s => Err(SecretKeyError::UnknownAlgorithm( - String::from_utf8_lossy(s).to_string(), - )), - } - } - - fn write(&self, buf: &mut E) { - let public = self.0.public_key(); - - buf.extend_ssh_string(b"ssh-ed25519"); - buf.extend_ssh_string(public.as_ref()); - buf.extend_ssh_string(self.0.as_ref()); - buf.extend_ssh_string(b"radicle"); - } -} - -#[cfg(test)] -mod test { - use std::sync::{Arc, Mutex}; - - use qcheck_macros::quickcheck; - - use crate as crypto; - use crate::{PublicKey, SecretKey}; - use radicle_ssh::agent::client::{AgentClient, ClientStream, Error}; - use radicle_ssh::encoding::*; - - #[derive(Clone, Default)] - struct DummyStream { - incoming: Arc>>, - } - - impl ClientStream for DummyStream { - fn request(&mut self, buf: &[u8]) -> Result { - *self.incoming.lock().unwrap() = buf.to_vec(); - - Ok(Buffer::default()) - } - } - - #[quickcheck] - fn prop_encode_decode_sk(input: [u8; 64]) { - let mut buf = Buffer::default(); - let sk = crypto::SecretKey::from(input); - sk.write(&mut buf); - - let mut cursor = buf.reader(0); - let output = SecretKey::read(&mut cursor).unwrap(); - - assert_eq!(sk, output); - } - - #[test] - fn test_agent_encoding_remove() { - use std::str::FromStr; - - let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap(); - let expected = [ - 0, 0, 0, 56, // Message length - 18, // Message type (remove identity) - 0, 0, 0, 51, // Key blob length - 0, 0, 0, 11, // Key type length - 115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, // Key type - 0, 0, 0, 32, // Key length - 208, 232, 92, 138, 225, 114, 116, 99, 156, 177, 148, 93, 65, 93, 198, 25, 46, 203, 79, - 37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key - ]; - - let stream = DummyStream::default(); - let mut agent = AgentClient::new(None, stream.clone()); - - agent.remove_identity(&pk).unwrap(); - - assert_eq!( - stream.incoming.lock().unwrap().as_slice(), - expected.as_slice() - ); - } - - #[test] - fn test_agent_encoding_sign() { - use std::str::FromStr; - - let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap(); - let expected = [ - 0, 0, 0, 73, // Message length - 13, // Message type (sign request) - 0, 0, 0, 51, // Key blob length - 0, 0, 0, 11, // Key type length - 115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, // Key type - 0, 0, 0, 32, // Public key - 208, 232, 92, 138, 225, 114, 116, 99, 156, 177, 148, 93, 65, 93, 198, 25, 46, 203, 79, - 37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key - 0, 0, 0, 9, // Length of data to sign - 1, 2, 3, 4, 5, 6, 7, 8, 9, // Data to sign - 0, 0, 0, 0, // Signature flags - ]; - - let stream = DummyStream::default(); - let mut agent = AgentClient::new(None, stream.clone()); - let data: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; - - agent.sign(&pk, &data).ok(); - - assert_eq!( - stream.incoming.lock().unwrap().as_slice(), - expected.as_slice() - ); - } -} diff --git a/crates/radicle-crypto/src/ssh/agent.rs b/crates/radicle-crypto/src/ssh/agent.rs index 864d53b9..93ea00b8 100644 --- a/crates/radicle-crypto/src/ssh/agent.rs +++ b/crates/radicle-crypto/src/ssh/agent.rs @@ -1,40 +1,109 @@ use std::cell::RefCell; +use std::env::VarError; use std::path::Path; +use std::path::PathBuf; -pub use radicle_ssh as ssh; -pub use ssh::agent::client::{AgentClient, Error}; +use proto::Credential; +use ssh_agent_lib::blocking::Client; +pub use ssh_agent_lib::error::AgentError; +use ssh_agent_lib::proto; +use ssh_key::public::{Ed25519PublicKey, KeyData}; +use thiserror::Error; use crate::{PublicKey, SecretKey, Signature, Signer}; use super::ExtendedSignature; +#[cfg(unix)] +use std::os::unix::net::UnixStream as Stream; + +#[cfg(windows)] +use winpipe::WinStream as Stream; + +#[derive(Debug, Error)] +pub enum ConnectError { + #[error(transparent)] + Agent(#[from] AgentError), + #[error("Unable to read environment variable '{var}': {source}")] + EnvVar { var: String, source: VarError }, +} + +impl ConnectError { + pub fn is_not_running(&self) -> bool { + use std::io::ErrorKind::*; + match self { + Self::EnvVar { + source: VarError::NotPresent, + .. + } => true, + Self::Agent(AgentError::IO(source)) if source.kind() == NotFound => true, + #[cfg(windows)] + Self::Agent(AgentError::IO(source)) if source.kind() == ConnectionRefused => { + // On Windows, a named pipe might be used, and if no + // agent is running, we might get a "connection refused" + // error, even though the `SSH_AUTH_SOCK` environment variable + // is set and a named pipe exists. + true + } + _ => false, + } + } +} + pub struct Agent { - client: AgentClient, + path: PathBuf, + client: Client, } impl Agent { /// Connect to a running SSH agent. - pub fn connect() -> Result { - Ok(Self { - client: AgentClient::connect_env()?, - }) + pub fn connect() -> Result { + const SSH_AUTH_SOCK: &str = "SSH_AUTH_SOCK"; + + let path = + PathBuf::from( + std::env::var(SSH_AUTH_SOCK).map_err(|err| ConnectError::EnvVar { + var: SSH_AUTH_SOCK.to_string(), + source: err, + })?, + ); + + let client = Client::new( + Stream::connect(&path).map_err(|err| ConnectError::Agent(AgentError::IO(err)))?, + ); + + Ok(Self { path, client }) } /// Register a key with the agent. - pub fn register(&mut self, key: &SecretKey) -> Result<(), ssh::Error> { - self.client.add_identity(key, &[]) + pub fn register(&mut self, key: &SecretKey) -> Result<(), AgentError> { + use ssh_key::private::{Ed25519Keypair, KeypairData}; + self.client.add_identity(proto::AddIdentity { + credential: Credential::Key { + privkey: KeypairData::Ed25519(Ed25519Keypair::from_bytes(key).unwrap()), + comment: "".into(), + }, + }) } - pub fn unregister(&mut self, key: &PublicKey) -> Result<(), ssh::Error> { - self.client.remove_identity(key) + pub fn unregister(&mut self, key: &PublicKey) -> Result<(), AgentError> { + self.client.remove_identity(proto::RemoveIdentity { + pubkey: Self::key_data(key), + }) } - pub fn unregister_all(&mut self) -> Result<(), ssh::Error> { + pub fn unregister_all(&mut self) -> Result<(), AgentError> { self.client.remove_all_identities() } - pub fn sign(&mut self, key: &PublicKey, data: &[u8]) -> Result<[u8; 64], ssh::Error> { - self.client.sign(key, data) + pub fn sign(&mut self, key: &PublicKey, data: &[u8]) -> Result<[u8; 64], AgentError> { + let sig = self.client.sign(proto::SignRequest { + pubkey: Self::key_data(key), + data: data.to_vec(), + flags: 0, + })?; + + Ok(sig.as_bytes().to_owned().try_into().unwrap()) } /// Get a signer from this agent, given the public key. @@ -42,12 +111,21 @@ impl Agent { AgentSigner::new(self, key) } - pub fn path(&self) -> Option<&Path> { - self.client.path() + pub fn path(&self) -> &Path { + self.path.as_ref() } - pub fn request_identities(&mut self) -> Result, ssh::agent::client::Error> { - self.client.request_identities() + pub fn request_identities(&mut self) -> Result, AgentError> { + Ok(self + .client + .request_identities()? + .into_iter() + .filter_map(|identity| identity.pubkey.ed25519().map(|key| PublicKey::from(key.0))) + .collect()) + } + + fn key_data(key: &PublicKey) -> KeyData { + KeyData::Ed25519(Ed25519PublicKey(***key)) } } @@ -95,7 +173,7 @@ impl AgentSigner { Self { agent, public } } - pub fn is_ready(&self) -> Result { + pub fn is_ready(&self) -> Result { let ids = self.agent.borrow_mut().request_identities()?; Ok(ids.contains(&self.public)) @@ -106,3 +184,79 @@ impl AgentSigner { Box::new(self) } } + +#[cfg(test)] +mod test { + use crate::PublicKey; + use ssh_agent_lib::blocking::Client; + use ssh_agent_lib::proto::SignRequest; + use ssh_agent_lib::ssh_key::public::{Ed25519PublicKey, KeyData}; + use std::ops::Deref; + + #[test] + fn test_agent_encoding_remove() { + use std::str::FromStr; + + let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap(); + let expected = [ + 0, 0, 0, 56, // Message length + 18, // Message type (remove identity) + 0, 0, 0, 51, // Key blob length + 0, 0, 0, 11, // Key type length + 115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, // Key type + 0, 0, 0, 32, // Key length + 208, 232, 92, 138, 225, 114, 116, 99, 156, 177, 148, 93, 65, 93, 198, 25, 46, 203, 79, + 37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key + ]; + + let mut client = Client::new(std::io::Cursor::new(vec![])); + + // We expect this to fail with an unexpected EOF, since the client will + // attempt to read a response from the stream, but the stream is empty, + // since we are not actually connected to SSH agent. + assert!( + matches!(client.remove_identity(ssh_agent_lib::proto::RemoveIdentity { + pubkey: KeyData::Ed25519(Ed25519PublicKey(**pk.deref())), + }), + Err( + super::AgentError::Proto(ssh_agent_lib::proto::ProtoError::IO(err)), + ) if err.kind() == std::io::ErrorKind::UnexpectedEof + ) + ); + + assert_eq!(client.into_inner().into_inner(), expected.as_slice()); + } + + #[test] + fn test_agent_encoding_sign() { + use std::str::FromStr; + + let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap(); + let expected = [ + 0, 0, 0, 73, // Message length + 13, // Message type (sign request) + 0, 0, 0, 51, // Key blob length + 0, 0, 0, 11, // Key type length + 115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, // Key type + 0, 0, 0, 32, // Public key + 208, 232, 92, 138, 225, 114, 116, 99, 156, 177, 148, 93, 65, 93, 198, 25, 46, 203, 79, + 37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key + 0, 0, 0, 9, // Length of data to sign + 1, 2, 3, 4, 5, 6, 7, 8, 9, // Data to sign + 0, 0, 0, 0, // Signature flags + ]; + + let mut client = Client::new(std::io::Cursor::new(vec![])); + let data: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; + + client + .sign(SignRequest { + pubkey: KeyData::Ed25519(Ed25519PublicKey(**pk.deref())), + data, + flags: 0, + }) + .ok(); + + assert_eq!(client.into_inner().into_inner(), expected); + } +} diff --git a/crates/radicle-crypto/src/test/signer.rs b/crates/radicle-crypto/src/test/signer.rs index b879fee0..59507bfb 100644 --- a/crates/radicle-crypto/src/test/signer.rs +++ b/crates/radicle-crypto/src/test/signer.rs @@ -1,4 +1,6 @@ -use crate::{KeyPair, PublicKey, SecretKey, Seed, Signature, ssh::ExtendedSignature}; +#[cfg(feature = "ssh")] +use crate::ssh::ExtendedSignature; +use crate::{KeyPair, PublicKey, SecretKey, Seed, Signature}; #[derive(Debug, Clone)] pub struct MockSigner { @@ -6,6 +8,7 @@ pub struct MockSigner { sk: SecretKey, } +#[cfg(feature = "ssh")] impl signature::Signer for MockSigner { fn try_sign(&self, msg: &[u8]) -> Result { use signature::Keypair as _; diff --git a/crates/radicle-ssh/Cargo.toml b/crates/radicle-ssh/Cargo.toml deleted file mode 100644 index 17c32abd..00000000 --- a/crates/radicle-ssh/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "radicle-ssh" -description = "Radicle SSH library" -homepage.workspace = true -repository.workspace = true -license = "Apache-2.0" -version = "0.10.0" -authors = [ - "Fintan Halpenny ", - "Pierre-Étienne Meunier ", - "cloudhead " -] -edition.workspace = true -rust-version.workspace = true - -[dependencies] -thiserror = { workspace = true, default-features = true } -zeroize = { workspace = true } - -[target.'cfg(windows)'.dependencies] -winpipe = { workspace = true } diff --git a/crates/radicle-ssh/LICENSE b/crates/radicle-ssh/LICENSE deleted file mode 100644 index f433b1a5..00000000 --- a/crates/radicle-ssh/LICENSE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/crates/radicle-ssh/src/agent.rs b/crates/radicle-ssh/src/agent.rs deleted file mode 100644 index fd54bf85..00000000 --- a/crates/radicle-ssh/src/agent.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// Write clients for SSH agents. -pub mod client; - -mod msg; - -/// Constraints on how keys can be used. -#[derive(Debug, PartialEq, Eq)] -pub enum Constraint { - /// The key shall disappear from the agent's memory after that many seconds. - KeyLifetime { seconds: u32 }, - /// Signatures need to be confirmed by the agent (for instance using a dialog). - Confirm, - /// Custom constraints - Extensions { name: Vec, details: Vec }, -} diff --git a/crates/radicle-ssh/src/agent/client.rs b/crates/radicle-ssh/src/agent/client.rs deleted file mode 100644 index 510b5f88..00000000 --- a/crates/radicle-ssh/src/agent/client.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::fmt; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; - -#[cfg(unix)] -pub use std::os::unix::net::UnixStream as Stream; - -#[cfg(windows)] -pub use winpipe::WinStream as Stream; - -use thiserror::Error; -use zeroize::Zeroize as _; - -use crate::agent::Constraint; -use crate::agent::msg; -use crate::encoding::{self, Encodable}; -use crate::encoding::{Buffer, Encoding, Reader}; - -/// An ed25519 Signature. -pub type Signature = [u8; 64]; - -#[derive(Debug, Error)] -pub enum Error { - /// Agent protocol error. - #[error("SSH agent replied with unexpected data, violating the SSH agent protocol.")] - AgentProtocolError, - #[error( - "SSH agent replied with failure (protocol message number 5), which could not be handled." - )] - AgentFailure, - #[error("Unable to connect to SSH agent because '{path}' was not found: {source}")] - BadAuthSock { - path: String, - source: std::io::Error, - }, - #[error("Encoding error while communicating with SSH agent: {0}")] - Encoding(#[from] encoding::Error), - #[error("Unable to read environment variable '{var}': {source}")] - EnvVar { - var: String, - source: std::env::VarError, - }, - #[error("Unable to connect SSH agent using the path '{path}': {source}")] - Connect { - path: String, - #[source] - source: std::io::Error, - }, - #[error("I/O error while communicating with SSH agent: {0}")] - Io(#[from] std::io::Error), -} - -impl Error { - pub fn is_not_running(&self) -> bool { - match self { - Self::EnvVar { .. } | Self::BadAuthSock { .. } => true, - #[cfg(windows)] - Self::Connect { source, .. } - if source.kind() == std::io::ErrorKind::ConnectionRefused => - { - // On Windows, a named pipe might be used, and if no - // agent is running, we might get a "connection refused" - // error, even though the `SSH_AUTH_SOCK` environment - // variable is set and the named pipe exists. - true - } - _ => false, - } - } -} - -/// SSH agent client. -pub struct AgentClient { - /// The path that was originally used to connect to the agent. - path: Option, - - /// The underlying stream to the SSH agent. - stream: S, -} - -impl AgentClient { - pub fn path(&self) -> Option<&Path> { - self.path.as_deref() - } -} - -impl AgentClient { - /// Connect to an SSH agent at the provided path. - pub fn connect

(path: P) -> Result - where - P: AsRef, - { - let path = path.as_ref().to_owned(); - - let stream = match Stream::connect(&path) { - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(Error::BadAuthSock { - path: path.display().to_string(), - source: err, - }); - } - Err(err) => { - return Err(Error::Connect { - path: path.display().to_string(), - source: err, - }); - } - Ok(stream) => stream, - }; - - Ok(Self { - path: Some(path), - stream, - }) - } - - pub fn connect_env() -> Result { - const SSH_AUTH_SOCK: &str = "SSH_AUTH_SOCK"; - - let var = std::env::var(SSH_AUTH_SOCK); - - #[cfg(windows)] - let var = var.or({ - // Windows uses a named pipe for the SSH agent, which - // we fall back to in case reading the environment - // variable fails. - Ok(r"\\.\pipe\openssh-ssh-agent".to_string()) - }); - - Self::connect(var.map_err(|err| Error::EnvVar { - var: SSH_AUTH_SOCK.to_string(), - source: err, - })?) - } -} - -impl AgentClient { - pub fn new(path: Option, stream: Stream) -> Self { - Self { path, stream } - } - - /// Send a key to the agent, with a (possibly empty) slice of constraints - /// to apply when using the key to sign. - pub fn add_identity(&mut self, key: &K, constraints: &[Constraint]) -> Result<(), Error> - where - K: Encodable, - K::Error: std::error::Error + Send + Sync + 'static, - { - let mut buf = Buffer::default(); - - buf.resize(4, 0); - - if constraints.is_empty() { - buf.push(msg::ADD_IDENTITY) - } else { - buf.push(msg::ADD_ID_CONSTRAINED) - } - key.write(&mut buf); - - if !constraints.is_empty() { - for cons in constraints { - match *cons { - Constraint::KeyLifetime { seconds } => { - buf.push(msg::CONSTRAIN_LIFETIME); - buf.extend_u32(seconds); - } - Constraint::Confirm => buf.push(msg::CONSTRAIN_CONFIRM), - Constraint::Extensions { - ref name, - ref details, - } => { - buf.push(msg::CONSTRAIN_EXTENSION); - buf.extend_ssh_string(name); - buf.extend_ssh_string(details); - } - } - } - } - buf.write_len(); - self.stream.request(&buf)?; - - Ok(()) - } - - /// Add a smart card to the agent, with a (possibly empty) set of - /// constraints to apply when signing. - pub fn add_smartcard_key( - &mut self, - id: &str, - pin: &[u8], - constraints: &[Constraint], - ) -> Result<(), Error> { - let mut buf = Buffer::default(); - - buf.resize(4, 0); - - if constraints.is_empty() { - buf.push(msg::ADD_SMARTCARD_KEY) - } else { - buf.push(msg::ADD_SMARTCARD_KEY_CONSTRAINED) - } - buf.extend_ssh_string(id.as_bytes()); - buf.extend_ssh_string(pin); - - if !constraints.is_empty() { - buf.extend_usize(constraints.len()); - for cons in constraints { - match *cons { - Constraint::KeyLifetime { seconds } => { - buf.push(msg::CONSTRAIN_LIFETIME); - buf.extend_u32(seconds); - } - Constraint::Confirm => buf.push(msg::CONSTRAIN_CONFIRM), - Constraint::Extensions { - ref name, - ref details, - } => { - buf.push(msg::CONSTRAIN_EXTENSION); - buf.extend_ssh_string(name); - buf.extend_ssh_string(details); - } - } - } - } - buf.write_len(); - self.stream.request(&buf)?; - - Ok(()) - } - - /// Lock the agent, making it refuse to sign until unlocked. - pub fn lock(&mut self, passphrase: &[u8]) -> Result<(), Error> { - let mut buf = Buffer::default(); - - buf.resize(4, 0); - buf.push(msg::LOCK); - buf.extend_ssh_string(passphrase); - buf.write_len(); - - self.stream.request(&buf)?; - - Ok(()) - } - - /// Unlock the agent, allowing it to sign again. - pub fn unlock(&mut self, passphrase: &[u8]) -> Result<(), Error> { - let mut buf = Buffer::default(); - buf.resize(4, 0); - buf.push(msg::UNLOCK); - buf.extend_ssh_string(passphrase); - buf.write_len(); - - self.stream.request(&buf)?; - - Ok(()) - } - - /// Ask the agent for a list of the currently registered secret - /// keys. - pub fn request_identities(&mut self) -> Result, Error> - where - K: Encodable, - K::Error: std::error::Error + Send + Sync + 'static, - { - let mut buf = Buffer::default(); - buf.resize(4, 0); - buf.push(msg::REQUEST_IDENTITIES); - buf.write_len(); - - let mut keys = Vec::new(); - let resp = self.stream.request(&buf)?; - - if resp[0] == msg::IDENTITIES_ANSWER { - let mut r = resp.reader(1); - let n = r.read_u32()?; - - for _ in 0..n { - let key = r.read_string()?; - let _ = r.read_string()?; - let mut r = key.reader(0); - - if let Ok(pk) = K::read(&mut r) { - keys.push(pk); - } - } - } - - Ok(keys) - } - - /// Ask the agent to sign the supplied piece of data. - pub fn sign(&mut self, public: &K, data: &[u8]) -> Result - where - K: Encodable + fmt::Debug, - { - let req = self.prepare_sign_request(public, data); - let resp = self.stream.request(&req)?; - - if !resp.is_empty() && resp[0] == msg::SIGN_RESPONSE { - self.read_signature(&resp) - } else if !resp.is_empty() && resp[0] == msg::FAILURE { - Err(Error::AgentFailure) - } else { - Err(Error::AgentProtocolError) - } - } - - fn prepare_sign_request(&self, public: &K, data: &[u8]) -> Buffer - where - K: Encodable + fmt::Debug, - { - // byte SSH_AGENTC_SIGN_REQUEST - // string key blob - // string data - // uint32 flags - - let mut pk = Buffer::default(); - public.write(&mut pk); - - let total = 1 + pk.len() + 4 + data.len() + 4; - - let mut buf = Buffer::default(); - buf.extend_usize(total); - buf.push(msg::SIGN_REQUEST); - buf.extend_from_slice(&pk); - buf.extend_ssh_string(data); - - // Signature flags should be zero for ed25519. - buf.extend_u32(0); - buf - } - - fn read_signature(&self, sig: &[u8]) -> Result { - let mut r = sig.reader(1); - let mut resp = r.read_string()?.reader(0); - let _t = resp.read_string()?; - let sig = resp.read_string()?; - - let mut out = [0; 64]; - out.copy_from_slice(sig); - - Ok(out) - } - - /// Ask the agent to remove a key from its memory. - pub fn remove_identity(&mut self, public: &K) -> Result<(), Error> - where - K: Encodable, - { - let mut pk: Buffer = Vec::new().into(); - public.write(&mut pk); - - let total = 1 + pk.len(); - - let mut buf = Buffer::default(); - buf.extend_usize(total); - buf.push(msg::REMOVE_IDENTITY); - buf.extend_from_slice(&pk); - - self.stream.request(&buf)?; - - Ok(()) - } - - /// Ask the agent to remove a smartcard from its memory. - pub fn remove_smartcard_key(&mut self, id: &str, pin: &[u8]) -> Result<(), Error> { - let mut buf = Buffer::default(); - buf.resize(4, 0); - buf.push(msg::REMOVE_SMARTCARD_KEY); - buf.extend_ssh_string(id.as_bytes()); - buf.extend_ssh_string(pin); - buf.write_len(); - - self.stream.request(&buf)?; - - Ok(()) - } - - /// Ask the agent to forget all known keys. - pub fn remove_all_identities(&mut self) -> Result<(), Error> { - let mut buf = Buffer::default(); - buf.resize(4, 0); - buf.push(msg::REMOVE_ALL_IDENTITIES); - buf.write_len(); - - self.stream.request(&buf)?; - - Ok(()) - } - - /// Send a custom message to the agent. - pub fn extension(&mut self, typ: &[u8], ext: &[u8]) -> Result<(), Error> { - let mut buf = Buffer::default(); - - buf.resize(4, 0); - buf.push(msg::EXTENSION); - buf.extend_ssh_string(typ); - buf.extend_ssh_string(ext); - buf.write_len(); - - self.stream.request(&buf)?; - - Ok(()) - } - - /// Ask the agent about supported extensions. - pub fn query_extension(&mut self, typ: &[u8], mut ext: Buffer) -> Result { - let mut req = Buffer::default(); - - req.resize(4, 0); - req.push(msg::EXTENSION); - req.extend_ssh_string(typ); - req.write_len(); - - let resp = self.stream.request(&req)?; - let mut r = resp.reader(1); - ext.extend(r.read_string()?); - - Ok(!resp.is_empty() && resp[0] == msg::SUCCESS) - } -} - -pub trait ClientStream: Sized + Send + Sync { - fn request(&mut self, msg: &[u8]) -> Result; -} - -impl ClientStream for S { - fn request(&mut self, msg: &[u8]) -> Result { - let mut resp = Buffer::default(); - - // Write the message - self.write_all(msg)?; - self.flush()?; - - // Read the length - resp.resize(4, 0); - self.read_exact(&mut resp)?; - - // Read the rest of the buffer - let len = u32::from_be_bytes(resp.as_slice().try_into().unwrap()) as usize; - - resp.zeroize(); - resp.resize(len, 0); - self.read_exact(&mut resp)?; - - Ok(resp) - } -} diff --git a/crates/radicle-ssh/src/agent/msg.rs b/crates/radicle-ssh/src/agent/msg.rs deleted file mode 100644 index d0786fd4..00000000 --- a/crates/radicle-ssh/src/agent/msg.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub const FAILURE: u8 = 5; -pub const SUCCESS: u8 = 6; -pub const IDENTITIES_ANSWER: u8 = 12; -pub const SIGN_RESPONSE: u8 = 14; -#[allow(dead_code)] -pub const EXTENSION_FAILURE: u8 = 28; - -pub const REQUEST_IDENTITIES: u8 = 11; -pub const SIGN_REQUEST: u8 = 13; -pub const ADD_IDENTITY: u8 = 17; -pub const REMOVE_IDENTITY: u8 = 18; -pub const REMOVE_ALL_IDENTITIES: u8 = 19; -pub const ADD_ID_CONSTRAINED: u8 = 25; -pub const ADD_SMARTCARD_KEY: u8 = 20; -pub const REMOVE_SMARTCARD_KEY: u8 = 21; -pub const LOCK: u8 = 22; -pub const UNLOCK: u8 = 23; -pub const ADD_SMARTCARD_KEY_CONSTRAINED: u8 = 26; -pub const EXTENSION: u8 = 27; - -pub const CONSTRAIN_LIFETIME: u8 = 1; -pub const CONSTRAIN_CONFIRM: u8 = 2; -pub const CONSTRAIN_EXTENSION: u8 = 3; diff --git a/crates/radicle-ssh/src/encoding.rs b/crates/radicle-ssh/src/encoding.rs deleted file mode 100644 index db3df0ac..00000000 --- a/crates/radicle-ssh/src/encoding.rs +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -use std::ops::DerefMut as _; - -use thiserror::Error; -use zeroize::Zeroizing; - -/// General purpose writable byte buffer we use everywhere. -pub type Buffer = Zeroizing>; - -#[derive(Debug, Error)] -pub enum Error { - /// Index out of bounds - #[error("Index out of bounds")] - IndexOutOfBounds, -} - -pub trait Encodable: Sized { - type Error: std::error::Error + Send + Sync + 'static; - - /// Read from the SSH format. - fn read(reader: &mut Cursor) -> Result; - /// Write to the SSH format. - fn write(&self, buf: &mut E); -} - -/// Encode in the SSH format. -pub trait Encoding { - /// Push an SSH-encoded string to `self`. - fn extend_ssh_string(&mut self, s: &[u8]); - /// Push an SSH-encoded blank string of length `s` to `self`. - fn extend_ssh_string_blank(&mut self, s: usize) -> &mut [u8]; - /// Push an SSH-encoded multiple-precision integer. - fn extend_ssh_mpint(&mut self, s: &[u8]); - /// Push an SSH-encoded list. - fn extend_list<'a, I: Iterator>(&mut self, list: I); - /// Push an SSH-encoded unsigned 32-bit integer. - fn extend_u32(&mut self, u: u32); - /// Push an SSH-encoded empty list. - fn write_empty_list(&mut self); - /// Write the buffer length at the beginning of the buffer. - fn write_len(&mut self); - /// Push a [`usize`] as an SSH-encoded unsigned 32-bit integer. - /// May panic if the argument is greater than [`u32::MAX`]. - /// This is a convenience method, to spare callers casting or converting - /// [`usize`] to [`u32`]. If callers end up in a situation where they - /// need to push a 32-bit unsigned integer, but the value they would - /// like to push does not fit 32 bits, then the implementation will not - /// comply with the SSH format anyway. - fn extend_usize(&mut self, u: usize) { - self.extend_u32(u.try_into().unwrap()) - } -} - -/// Encoding length of the given mpint. -pub fn mpint_len(s: &[u8]) -> usize { - let mut i = 0; - while i < s.len() && s[i] == 0 { - i += 1 - } - (if s[i] & 0x80 != 0 { 5 } else { 4 }) + s.len() - i -} - -impl Encoding for Vec { - fn extend_ssh_string(&mut self, s: &[u8]) { - self.extend_usize(s.len()); - self.extend(s); - } - - fn extend_ssh_string_blank(&mut self, len: usize) -> &mut [u8] { - self.extend_usize(len); - let current = self.len(); - self.resize(current + len, 0u8); - - &mut self[current..] - } - - fn extend_ssh_mpint(&mut self, s: &[u8]) { - // Skip initial 0s. - let mut i = 0; - while i < s.len() && s[i] == 0 { - i += 1 - } - // If the first non-zero is >= 128, write its length (u32, BE), followed by 0. - if s[i] & 0x80 != 0 { - self.extend_usize(s.len() - i + 1); - self.push(0) - } else { - self.extend_usize(s.len() - i); - } - self.extend(&s[i..]); - } - - fn extend_u32(&mut self, s: u32) { - self.extend(s.to_be_bytes()); - } - - fn extend_list<'a, I: Iterator>(&mut self, list: I) { - let len0 = self.len(); - - let mut first = true; - for i in list { - if !first { - self.push(b',') - } else { - first = false; - } - self.extend(i) - } - let len = (self.len() - len0 - 4) as u32; - - self.splice(len0..len0, len.to_be_bytes()); - } - - fn write_empty_list(&mut self) { - self.extend([0, 0, 0, 0]); - } - - fn write_len(&mut self) { - let len = self.len() - 4; - self[..4].copy_from_slice((len as u32).to_be_bytes().as_slice()); - } -} - -impl Encoding for Buffer { - fn extend_ssh_string(&mut self, s: &[u8]) { - self.deref_mut().extend_ssh_string(s) - } - - fn extend_ssh_string_blank(&mut self, len: usize) -> &mut [u8] { - self.deref_mut().extend_ssh_string_blank(len) - } - - fn extend_ssh_mpint(&mut self, s: &[u8]) { - self.deref_mut().extend_ssh_mpint(s) - } - - fn extend_list<'a, I: Iterator>(&mut self, list: I) { - self.deref_mut().extend_list(list) - } - - fn write_empty_list(&mut self) { - self.deref_mut().write_empty_list() - } - - fn extend_u32(&mut self, s: u32) { - self.deref_mut().extend_u32(s); - } - - fn write_len(&mut self) { - self.deref_mut().write_len() - } -} - -/// A cursor-like trait to read SSH-encoded things. -pub trait Reader { - /// Create an SSH reader for `self`. - fn reader(&self, starting_at: usize) -> Cursor<'_>; -} - -impl Reader for Buffer { - fn reader(&self, starting_at: usize) -> Cursor<'_> { - Cursor { - s: self, - position: starting_at, - } - } -} - -impl Reader for [u8] { - fn reader(&self, starting_at: usize) -> Cursor<'_> { - Cursor { - s: self, - position: starting_at, - } - } -} - -/// A cursor-like type to read SSH-encoded values. -#[derive(Debug)] -pub struct Cursor<'a> { - s: &'a [u8], - #[doc(hidden)] - pub position: usize, -} - -impl<'a> Cursor<'a> { - /// Read one string from this reader. - pub fn read_string(&mut self) -> Result<&'a [u8], Error> { - let len = self.read_u32()? as usize; - if self.position + len <= self.s.len() { - let result = &self.s[self.position..(self.position + len)]; - self.position += len; - Ok(result) - } else { - Err(Error::IndexOutOfBounds) - } - } - - /// Read a `u32` from this reader. - pub fn read_u32(&mut self) -> Result { - if self.position + 4 <= self.s.len() { - let u = - u32::from_be_bytes(self.s[self.position..self.position + 4].try_into().unwrap()); - self.position += 4; - Ok(u) - } else { - Err(Error::IndexOutOfBounds) - } - } - - /// Read one byte from this reader. - pub fn read_byte(&mut self) -> Result { - if self.position < self.s.len() { - let u = self.s[self.position]; - self.position += 1; - Ok(u) - } else { - Err(Error::IndexOutOfBounds) - } - } - - pub fn read_bytes(&mut self) -> Result<[u8; S], Error> { - let mut buf = [0; S]; - for b in buf.iter_mut() { - *b = self.read_byte()?; - } - Ok(buf) - } - - /// Read one byte from this reader. - pub fn read_mpint(&mut self) -> Result<&'a [u8], Error> { - let len = self.read_u32()? as usize; - if self.position + len <= self.s.len() { - let result = &self.s[self.position..(self.position + len)]; - self.position += len; - Ok(result) - } else { - Err(Error::IndexOutOfBounds) - } - } -} diff --git a/crates/radicle-ssh/src/lib.rs b/crates/radicle-ssh/src/lib.rs deleted file mode 100644 index bd07d900..00000000 --- a/crates/radicle-ssh/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod agent; -pub mod encoding; - -pub use agent::client::Error; diff --git a/crates/radicle/Cargo.toml b/crates/radicle/Cargo.toml index 850adb80..67e7dc88 100644 --- a/crates/radicle/Cargo.toml +++ b/crates/radicle/Cargo.toml @@ -50,7 +50,6 @@ radicle-git-ref-format = { workspace = true, features = ["macro", "serde"] } radicle-git-metadata = { workspace = true } radicle-localtime = { workspace = true, features = ["serde"] } radicle-oid = { workspace = true, features = ["git2", "serde", "std", "sha1"] } -radicle-ssh = { workspace = true } schemars = { workspace = true, optional = true, features = ["derive", "std"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["preserve_order"] } diff --git a/crates/radicle/src/profile.rs b/crates/radicle/src/profile.rs index bf029b1d..fc557910 100644 --- a/crates/radicle/src/profile.rs +++ b/crates/radicle/src/profile.rs @@ -199,7 +199,7 @@ pub enum SignerError { MemorySigner(#[from] keystore::MemorySignerError), #[error(transparent)] - Agent(#[from] crate::crypto::ssh::agent::Error), + Agent(#[from] crate::crypto::ssh::agent::AgentError), #[error("radicle key `{0}` is not registered; run `rad auth` to register it with ssh-agent")] KeyNotRegistered(PublicKey), @@ -209,7 +209,7 @@ pub enum SignerError { #[error("error connecting to ssh-agent: {source}")] AgentConnection { - source: crate::crypto::ssh::agent::Error, + source: crate::crypto::ssh::agent::ConnectError, }, }