summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIgor Matuszewski <igor@sequoia-pgp.org>2020-10-24 01:20:36 +0200
committerIgor Matuszewski <igor@sequoia-pgp.org>2020-11-01 19:08:00 +0100
commit4ea6fb50f5a83aeb9661ff8e523ea698211f3924 (patch)
tree8b8e758c9a5a8354d9fee417af8737fe4cf4dfcf
parent0659770a8fbb99e7311c864ffc96ee28c55100e1 (diff)
ipc: Support UDS emulation as TCP + nonce for Assuan protocol
-rw-r--r--Cargo.lock17
-rw-r--r--ipc/Cargo.toml3
-rw-r--r--ipc/src/assuan/mod.rs14
-rw-r--r--ipc/src/assuan/socket.rs165
4 files changed, 139 insertions, 60 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 94ff7c37..d774617e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1438,22 +1438,6 @@ dependencies = [
]
[[package]]
-name = "parity-tokio-ipc"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1d417ba1ab454723ff2271bf999fd700027dc48759a13d43e488cc8ca38b87f"
-dependencies = [
- "futures",
- "libc",
- "log",
- "mio-named-pipes",
- "miow 0.3.5",
- "rand",
- "tokio",
- "winapi 0.3.9",
-]
-
-[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1865,7 +1849,6 @@ dependencies = [
"lazy_static",
"libc",
"memsec",
- "parity-tokio-ipc",
"quickcheck",
"rand",
"sequoia-core",
diff --git a/ipc/Cargo.toml b/ipc/Cargo.toml
index dc680257..3988506f 100644
--- a/ipc/Cargo.toml
+++ b/ipc/Cargo.toml
@@ -35,9 +35,8 @@ memsec = { version = "0.6", default-features = false }
rand = { version = "0.7", default-features = false }
tempfile = "3.0"
thiserror = "1"
-tokio = { version = "0.2", features = ["rt-core", "rt-util", "tcp"] }
+tokio = { version = "0.2", features = ["rt-core", "rt-util", "tcp", "uds", "io-util", "macros"] }
tokio-util = { version = "0.3", features = ["compat"] }
-parity-tokio-ipc = "0.7"
socket2 = "0.3.11"
[target.'cfg(windows)'.dependencies]
diff --git a/ipc/src/assuan/mod.rs b/ipc/src/assuan/mod.rs
index 916d01c9..55be0d66 100644
--- a/ipc/src/assuan/mod.rs
+++ b/ipc/src/assuan/mod.rs
@@ -12,7 +12,6 @@ use std::task::{Poll, Context};
use lalrpop_util::ParseError;
use futures::{Future, Stream, StreamExt};
-use parity_tokio_ipc::Connection;
use tokio::io::{BufReader, ReadHalf, WriteHalf};
use tokio::io::{AsyncRead, AsyncWriteExt};
@@ -23,6 +22,7 @@ use crate::Result;
mod lexer;
mod socket;
+use socket::IpcStream;
// Maximum line length of the reference implementation.
const MAX_LINE_LENGTH: usize = 1000;
@@ -61,15 +61,15 @@ lalrpop_util::lalrpop_mod!(
/// [`Connection::data()`]: #method.data
/// [`Connection::cancel()`]: #method.cancel
pub struct Client {
- r: BufReader<ReadHalf<Connection>>, // xxx: abstract over
+ r: BufReader<ReadHalf<IpcStream>>, // xxx: abstract over
buffer: Vec<u8>,
done: bool,
w: WriteState,
}
enum WriteState {
- Ready(WriteHalf<Connection>),
- Sending(Pin<Box<dyn Future<Output = Result<WriteHalf<Connection>, anyhow::Error>>>>),
+ Ready(WriteHalf<IpcStream>),
+ Sending(Pin<Box<dyn Future<Output = Result<WriteHalf<IpcStream>, anyhow::Error>>>>),
Transitioning,
Dead,
}
@@ -77,9 +77,7 @@ enum WriteState {
impl Client {
/// Connects to the server.
pub async fn connect<P>(path: P) -> Result<Client> where P: AsRef<Path> {
- // XXX: Implement Windows support using TCP + nonce approach used upstream
- // https://gnupg.org/documentation/manuals/assuan.pdf#Socket%20wrappers
- let connection = parity_tokio_ipc::Endpoint::connect(path).await?;
+ let connection = socket::sock_connect(path)?;
Ok(ConnectionFuture::new(connection).await?)
}
@@ -194,7 +192,7 @@ impl Client {
struct ConnectionFuture(Option<Client>);
impl ConnectionFuture {
- fn new(c: Connection) -> Self {
+ fn new(c: IpcStream) -> Self {
let (r, w) = tokio::io::split(c);
let buffer = Vec::with_capacity(MAX_LINE_LENGTH);
Self(Some(Client {
diff --git a/ipc/src/assuan/socket.rs b/ipc/src/assuan/socket.rs
index 92b913b1..1e69cb39 100644
--- a/ipc/src/assuan/socket.rs
+++ b/ipc/src/assuan/socket.rs
@@ -2,32 +2,132 @@
//!
//! [assuan-socket.c]: https://github.com/gpg/libassuan/blob/master/src/assuan-socket.c
-use std::io;
+// Makes writing platform-specific code less verbose (less #[cfgs] everywhere)
+#![allow(dead_code, unused_imports)]
+
+use std::io::{Write, Read};
use std::path::Path;
+use std::fs::File;
+
+use anyhow::anyhow;
use crate::Result;
+#[cfg(windows)]
+pub(crate) type IpcStream = tokio::net::TcpStream;
+#[cfg(unix)]
+pub(crate) type IpcStream = tokio::net::UnixStream;
+
+/// Connects to a local socket, returning a Tokio-enabled async connection.
+///
+/// Supports regular local domain sockets under Unix-like systems and
+/// either Cygwin or libassuan's socket emulation on Windows.
+///
+/// # Panic
+///
+/// This function panics if not called from within a Tokio runtime.
+pub(crate) fn sock_connect(path: impl AsRef<Path>) -> Result<IpcStream> {
+
+ #[cfg(unix)]
+ {
+ let stream = std::os::unix::net::UnixStream::connect(path)?;
+ stream.set_nonblocking(true)?;
+ Ok(tokio::net::UnixStream::from_std(stream)?)
+ }
+ #[cfg(windows)]
+ {
+ use std::net::{Ipv4Addr, TcpStream};
+
+ let rendezvous = read_port_and_nonce(path.as_ref())?;
+ let Rendezvous { port, uds_emulation, nonce } = rendezvous;
+
+ let mut stream = TcpStream::connect((Ipv4Addr::LOCALHOST, port))?;
+ stream.set_nodelay(true)?;
+
+ // Authorize ourselves with nonce read from the file
+ stream.write(&nonce)?;
+
+ if let UdsEmulation::Cygwin = uds_emulation {
+ // The client sends the nonce back - not useful. Do a dummy read
+ stream.read_exact(&mut [0u8; 16])?;
+
+ // Send our credentials as expected by libassuan:
+ // [ pid |uid|gid] (8 bytes)
+ // [_|_|_|_|_|_|_|_]
+ let mut creds = [0u8; 8]; // uid = gid = 0
+ creds[..4].copy_from_slice(&std::process::id().to_ne_bytes());
+ stream.write_all(&creds)?;
+ // FIXME: libassuan in theory reads only 8 bytes here, but
+ // somehow 12 have to be written for the server to progress (tested
+ // on mingw-x86_64-gnupg).
+ // My bet is that mingw socket API does that transparently instead
+ // and expects to read an actual `ucred` struct (socket.h) which is
+ // three `__u32`s.
+ // Here, neither client nor server uses it, so just send dummy bytes
+ stream.write_all(&[0u8; 4])?;
+
+ // Receive back credentials. We don't need them.
+ stream.read_exact(&mut [0u8; 12])?;
+ }
+
+ stream.set_nonblocking(true)?;
+ Ok(tokio::net::TcpStream::from_std(stream)?)
+ }
+}
+
+/// Socket connection data.
#[derive(Debug)]
-pub(crate) struct Rendezvous {
+struct Rendezvous {
port: u16,
- socket_kind: SocketKind,
+ uds_emulation: UdsEmulation,
nonce: [u8; 16],
}
+/// Unix domain socket emulation type (Windows only).
+///
+/// Until Windows 10 Update 1803, Windows did not support native UNIX domain
+/// sockets. To work around that, developers historically used TCP (readily
+/// available) connection coupled with an authentication nonce (or "cookie").
#[derive(Debug)]
-pub(crate) enum SocketKind {
+enum UdsEmulation {
+ /// Cygwin socket emulation.
+ ///
+ /// File format: `!<socket >%u %c %08x-%08x-%08x-%08x` (scanf style)
+ /// %u: local TCP port
+ /// %c: socket type ("s" for `SOCK_STREAM`, "d" for `SOCK_DGRAM`)
+ /// %08x-%08x-%08x-%08x: authentication nonce
+ ///
+ /// Starting with client, both sides first exchange the 16-byte authentication
+ /// nonce, after which they exchange `ucred` structure (socket.h).
Cygwin,
- Emulated
+ /// Libassuan's custom socket emulation.
+ ///
+ /// File format: `<PORT>\n<NONCE>`
+ /// PORT: textual local TCP port (e.g. "12345")
+ /// NONCE: raw 16-byte authentication nonce
+ ///
+ /// After connecting, client has to authenticate itself by sending the
+ /// 16-byte authentication nonce.
+ Libassuan,
}
-pub(crate) fn read_port_and_nonce(fname: &Path) -> Result<Rendezvous> {
- let contents = std::fs::read_to_string(fname)?;
+/// Reads socket connection info from a Windows file emulating a Unix socket.
+///
+/// Inspired by `read_port_and nonce` from assuan-socket.c.
+fn read_port_and_nonce(fname: &Path) -> Result<Rendezvous> {
+ let mut file = File::open(fname)?;
+ // Socket connection info will be in either a <= 54 byte long Cygwin format
+ // or ~5+1+16 (modulo whitespace separators) custom libassuan format
+ let mut contents = Vec::with_capacity(64);
+ file.read_to_end(&mut contents)?;
read_port_and_nonce_from_string(&contents)
}
-fn read_port_and_nonce_from_string(contents: &str) -> Result<Rendezvous> {
- match contents.strip_prefix("!<socket >") {
+fn read_port_and_nonce_from_string(contents: &[u8]) -> Result<Rendezvous> {
+ let maybe_utf8 = std::str::from_utf8(contents).ok();
+
+ match maybe_utf8.and_then(|buf| buf.strip_prefix("!<socket >")) {
// libassuan's Cygwin compatible socket emulation.
// Format: "!<socket >%u %c %08x-%08x-%08x-%08x\x00" (scanf-like)
Some(buf) => {
@@ -37,10 +137,10 @@ fn read_port_and_nonce_from_string(contents: &str) -> Result<Rendezvous> {
match (iter.next(), iter.next(), iter.next()) {
(Some(port), Some("s"), Some(nonce)) => {
let port = port.parse()?;
- let socket_kind = SocketKind::Cygwin;
- // This is wasteful but an allocation-free alternative is even
- // more verbose and it's not enough to pull a hex parser dep.
+ // This is wasteful but an allocation-free alternative is
+ // even more verbose and also does not warrant pulling a
+ // hex string parser dependency.
let nonce_chunks = nonce.split_terminator('-')
.map(|dword| u32::from_str_radix(dword, 16).map_err(Into::into))
.collect::<Result<Vec<_>>>();
@@ -54,27 +154,26 @@ fn read_port_and_nonce_from_string(contents: &str) -> Result<Rendezvous> {
nonce[12..16].copy_from_slice(&d3.to_ne_bytes());
nonce
},
- _ => return Err(anyhow::anyhow!("Couldn't parse Cygwin socket nonce: {}", contents)),
+ _ => return Err(anyhow!("Couldn't parse Cygwin socket nonce: {}", nonce)),
};
- Ok(Rendezvous { port, nonce, socket_kind })
+ Ok(Rendezvous { port, nonce, uds_emulation: UdsEmulation::Cygwin })
},
- _ => return Err(anyhow::anyhow!("Couldn't parse Cygwin socket: {}", contents)),
+ _ => return Err(anyhow!("Couldn't parse Cygwin socket: {}", buf)),
}
},
// libassuan's own socket emulation
// Format: [<whitespace>?, port, .., '\n', <16 byte nonce>]
None => {
- let pos = match contents.as_bytes().iter().position(|&x| x == b'\n') {
+ let pos = match contents.iter().position(|&x| x == b'\n') {
// Also ensure that there are exactly 16 bytes following
Some(pos) if pos + 1 + 16 == contents.len() => pos,
- _ => return Err(anyhow::anyhow!("Malformed socket description: {}", contents)),
+ _ => return Err(anyhow!("Malformed socket description: {:?}", contents)),
};
- let port = contents[..pos].trim().parse()?;
+ let port = std::str::from_utf8(&contents[..pos])?.trim().parse()?;
let mut nonce = [0u8; 16];
- nonce[..].copy_from_slice(&contents.as_bytes()[pos + 1..]);
- let socket_kind = SocketKind::Emulated;
+ nonce[..].copy_from_slice(&contents[pos + 1..]);
- Ok(Rendezvous { port, nonce, socket_kind: SocketKind::Emulated })
+ Ok(Rendezvous { port, nonce, uds_emulation: UdsEmulation::Libassuan })
}
}
}
@@ -84,30 +183,30 @@ mod tests {
use super::*;
#[test]
- fn read_port_and_nonce_from_files() -> Result<()> {
- let test_fn = read_port_and_nonce_from_string;
- assert!(test_fn("\t 12 \n1234567890123456").is_ok());
- assert!(test_fn("\t 12 \n123456789012345").is_err());
- assert!(test_fn("\t 12 \n12345678901234567").is_err());
+ fn read_port_and_nonce() -> Result<()> {
+ let test_fn = super::read_port_and_nonce_from_string;
+ assert!(test_fn(b"\t 12 \n1234567890123456").is_ok());
+ assert!(test_fn(b"\t 12 \n123456789012345").is_err());
+ assert!(test_fn(b"\t 12 \n12345678901234567").is_err());
assert!(matches!(
- test_fn(" 12345\n\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"),
+ test_fn(b" 12345\n\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"),
Ok(Rendezvous {
port: 12345,
- socket_kind: SocketKind::Emulated,
+ uds_emulation: UdsEmulation::Libassuan,
nonce: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
})
));
assert!(matches!(
- test_fn(" -152\n\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"),
+ test_fn(b" -152\n\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"),
Err(..)
));
assert!(matches!(
- test_fn("!<socket >12345 s AABBCCDD-DDCCBBAA-01234567-890ABCDE\x00"),
+ test_fn(b"!<socket >12345 s AABBCCDD-DDCCBBAA-01234567-890ABCDE\x00"),
Ok(Rendezvous {
port: 12345,
- socket_kind: SocketKind::Cygwin,
+ uds_emulation: UdsEmulation::Cygwin,
nonce: [
0xDD, 0xCC, 0xBB, 0xAA,
0xAA, 0xBB, 0xCC, 0xDD,
@@ -117,10 +216,10 @@ mod tests {
})
));
assert!(matches!(
- test_fn("!<socket >12345 s AABBCCDD-DDCCBBAA-01234567-890ABCDE"),
+ test_fn(b"!<socket >12345 s AABBCCDD-DDCCBBAA-01234567-890ABCDE"),
Ok(Rendezvous {
port: 12345,
- socket_kind: SocketKind::Cygwin,
+ uds_emulation: UdsEmulation::Cygwin,
nonce: [
0xDD, 0xCC, 0xBB, 0xAA,
0xAA, 0xBB, 0xCC, 0xDD,