mirror of
https://github.com/danog/tergent.git
synced 2024-11-26 12:04:49 +01:00
Initial commit
This commit is contained in:
commit
3ae3248ed3
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
**/*.rs.bk
|
164
Cargo.lock
generated
Normal file
164
Cargo.lock
generated
Normal file
@ -0,0 +1,164 @@
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon-sys"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ryu 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.77 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"arc-swap 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tergent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"signal-hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[metadata]
|
||||
"checksum arc-swap 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f753d9b7c861f9f426fdb10479e35ffef7eaa4359d7c3595610645459df8849a"
|
||||
"checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643"
|
||||
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
|
||||
"checksum byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90492c5858dd7d2e78691cfb89f90d273a2800fc11d98f60786e5d87e2f83781"
|
||||
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
"checksum hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77"
|
||||
"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
|
||||
"checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d"
|
||||
"checksum rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e464cd887e869cddcae8792a4ee31d23c7edd516700695608f5b98c67ee0131c"
|
||||
"checksum rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "edecf0f94da5551fc9b492093e30b041a891657db7940ee221f9d2f66e82eef2"
|
||||
"checksum ryu 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7153dd96dade874ab973e098cb62fcdbb89a03682e46b144fd09550998d4a4a7"
|
||||
"checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9"
|
||||
"checksum serde 1.0.77 (registry+https://github.com/rust-lang/crates.io-index)" = "c6e67977d7523ce4d9284ed58918af99392de8edb6192c44afefcf634654ab7f"
|
||||
"checksum serde_json 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "59790990c5115d16027f00913e2e66de23a51f70422e549d2ad68c8c5f268f1c"
|
||||
"checksum signal-hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f7ca1f1c0ed6c8beaab713ad902c041e4f09d06e1b4bb74c5fc553c078ed0110"
|
||||
"checksum winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "tergent"
|
||||
version = "0.1.0"
|
||||
authors = ["Kaan Karaagacli <kaankaraagacli@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
hex = "0.3.2"
|
||||
rand = "0.5.5"
|
||||
byteorder = "1"
|
||||
base64 = "0.9.3"
|
||||
signal-hook = "0.1.5"
|
50
README.md
Normal file
50
README.md
Normal file
@ -0,0 +1,50 @@
|
||||
tergent - termux ssh agent
|
||||
--------------------------
|
||||
|
||||
An [ssh-agent implementation](https://tools.ietf.org/id/draft-miller-ssh-agent-02.html) for [Termux](https://termux.com/) that uses [Android Keystore](https://developer.android.com/training/articles/keystore) as its backend.
|
||||
|
||||
This application enables the use of keys securely stored in termux-api with ssh-agent protocol capable clients. These clients include the applications provided by openssh, such as `ssh`, `scp`, `ssh-add` and `ssh-copy-id`.
|
||||
|
||||
Tergent does not (and cannot) access your private keys as they are stored inside the secure hardware. In fact, they can never leave the chip even with root privileges, thanks to [extraction preventation](https://developer.android.com/training/articles/keystore#ExtractionPrevention).
|
||||
Cryptographic actions are performed by the hardware itself.
|
||||
|
||||
Compiling
|
||||
---------
|
||||
Install [Rust](https://www.rust-lang.org/en-US/install.html) and [Android NDK](https://developer.android.com/ndk/).
|
||||
You will need to configure cargo with the correct locations for "ar" and "linker", you can follow this page up to and including the `rustup target add ...` command:
|
||||
[https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html](https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html)
|
||||
Then this project can be compiled with the command `cargo build --target=aarch64-linux-android` (or any other Android target).
|
||||
|
||||
Alternatively, you can download a debug build for ARM64 Android from the releases page.
|
||||
|
||||
Usage
|
||||
-----
|
||||
1. (This step will be updated if the patches to termux are merged)
|
||||
Make sure you have termux-api app with [this patch](https://github.com/aeolwyr/termux-api/commit/ef3a0af0f6cebf667385034be2c5698c7d3946f2) installed. Inside Termux, install the latest version of the termux-api package using `pkg install termux-api`. Finally, grab a copy of `termux-keystore` script from [this fork](https://github.com/aeolwyr/termux-api-package/blob/master/scripts/termux-keystore).
|
||||
|
||||
2. Generate a key using the command `./termux-keystore generate myAlias`, where myAlias is the name you want to give to the key.
|
||||
- This command creates an RSA key - to create an ECDSA key, use the argument "-a EC": `./termux-keystore generate myAlias -a EC`.
|
||||
- To specify a key size, use the argument "-s", e.g. `./termux-keystore generate myAlias -s 4096` or `./termux-keystore generate myAlias -a EC -s 521`.
|
||||
|
||||
3. Verify the key is generated by running the command `./termux-keystore list`.
|
||||
|
||||
4. Run tergent with this command: `eval $(./tergent)`.
|
||||
|
||||
5. Again list the keys to verify, but now using the standard ssh tool: `ssh-add -l`.
|
||||
|
||||
6. Copy the public key to your server. For this, you have two options:
|
||||
- ssh-copy-id is the standard tool for this - invoke it by running `ssh-copy-id example.com`.
|
||||
- You can also use `ssh-add -L` (notice the uppercase L), and manually copy and append the output to the `.ssh/authorized_keys` file on your server.
|
||||
|
||||
7. Connect to your server using the usual command `ssh example.com`.
|
||||
|
||||
You will need to run the command `eval $(./tergent)` every time you start up the terminal. This command can be included in .bash_profile (or a similar script) for convenience.
|
||||
|
||||
How do I...
|
||||
-----------
|
||||
* **list keys**: run either `ssh-add -l` or `termux-keystore list`
|
||||
* **create a new key**: use `termux-keystore generate`
|
||||
* **use a key**: just run `ssh`
|
||||
* **delete a key**: use `termux-keystore delete`
|
||||
* **import a key**: not supported, generate a new key instead
|
||||
|
38
src/bridge.rs
Normal file
38
src/bridge.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use base64;
|
||||
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// Location of the `termux-api` application.
|
||||
const PROGRAM: &str = "/data/data/com.termux/files/usr/libexec/termux-api";
|
||||
|
||||
/// Send a request to `termux-api` to list all the keys.
|
||||
pub fn list_keys() -> String {
|
||||
let output = Command::new(PROGRAM)
|
||||
.args(&["Keystore", "-e", "command", "list"])
|
||||
.args(&["--ez", "detailed", "true"])
|
||||
.output()
|
||||
.expect("Could not execute the termux-api program.");
|
||||
|
||||
String::from_utf8(output.stdout)
|
||||
.expect("Malformed response received from termux-api.")
|
||||
}
|
||||
|
||||
/// Send some data to `termux-api` to be signed.
|
||||
/// Algorithm parameter must be in the format that keystore expects
|
||||
/// (e.g. "SHA512withRSA").
|
||||
pub fn sign(alias: &str, algorithm: &str, data: &[u8]) -> Vec<u8> {
|
||||
let mut child = Command::new(PROGRAM)
|
||||
.args(&["Keystore", "-e", "command", "sign"])
|
||||
.args(&["-e", "alias", alias, "-e", "algorithm", algorithm])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Could not execute the termux-api program.");
|
||||
|
||||
child.stdin.as_mut().and_then(|i| i.write_all(data).ok())
|
||||
.expect("Could not send the data to termux-api.");
|
||||
|
||||
child.wait_with_output().ok().and_then(|o| base64::decode(&o.stdout).ok())
|
||||
.expect("Could not read the signature response from termux-api.")
|
||||
}
|
48
src/key.rs
Normal file
48
src/key.rs
Normal file
@ -0,0 +1,48 @@
|
||||
/// A key instance, representing a key stored inside the keystore.
|
||||
pub struct Key {
|
||||
pub algorithm: Algorithm,
|
||||
pub alias: String,
|
||||
}
|
||||
|
||||
pub enum Algorithm {
|
||||
Rsa, Ec(Curve),
|
||||
}
|
||||
|
||||
/// Available elliptic curves, each representing a NIST P curve.
|
||||
pub enum Curve {
|
||||
P256, P384, P521,
|
||||
}
|
||||
|
||||
impl Algorithm {
|
||||
/// Tries to find the corrensponding algorithm given the fields
|
||||
/// acquired from `termux-api`. Returns an empty response if
|
||||
/// either the algorithm is unknown or the key size is incompatible
|
||||
/// with the algorithm.
|
||||
pub fn parse(algorithm: &str, size: u64) -> Option<Algorithm> {
|
||||
match algorithm {
|
||||
// In SSH, RSA only has one key type that represents all sizes.
|
||||
"RSA" => Some(Algorithm::Rsa),
|
||||
"EC" => {
|
||||
let curve = match size {
|
||||
256 => Curve::P256,
|
||||
384 => Curve::P384,
|
||||
521 => Curve::P521,
|
||||
_ => return None, // unknown curve
|
||||
};
|
||||
Some(Algorithm::Ec(curve))
|
||||
},
|
||||
_ => None, // unknown algorithm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
/// Returns the full curve name, e.g. "nistp256".
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Curve::P256 => "nistp256",
|
||||
Curve::P384 => "nistp384",
|
||||
Curve::P521 => "nistp521",
|
||||
}
|
||||
}
|
||||
}
|
35
src/list.rs
Normal file
35
src/list.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use std::io::{Write, Result};
|
||||
use byteorder::{BE, WriteBytesExt};
|
||||
|
||||
use super::KeyStore;
|
||||
|
||||
// defined in the specification document
|
||||
const SSH_AGENT_IDENTITIES_ANSWER: u8 = 12;
|
||||
|
||||
/// Prints the information about all the keys stored in the keystore
|
||||
/// to the given stream, in the format expected by a ssh-agent client.
|
||||
pub fn write_list_response(store: &KeyStore, stream: &mut Write) -> Result<()> {
|
||||
// total length of the keys
|
||||
let length: u32 = store.iter().map(|(blob, key)| {
|
||||
// blob already includes its header
|
||||
// need 4 for alias header
|
||||
(blob.len() as u32) + 4 + (key.alias.len() as u32)
|
||||
}).sum();
|
||||
|
||||
// total packet size: +1 for message type and +4 for number of keys
|
||||
stream.write_u32::<BE>(length + 5)?;
|
||||
// message type
|
||||
stream.write_u8(SSH_AGENT_IDENTITIES_ANSWER)?;
|
||||
// number of keys
|
||||
stream.write_u32::<BE>(store.len() as u32)?;
|
||||
|
||||
for (blob, key) in store {
|
||||
// blob (includes size inside)
|
||||
stream.write_all(blob)?;
|
||||
// alias
|
||||
stream.write_u32::<BE>(key.alias.len() as u32)?;
|
||||
stream.write_all(key.alias.as_bytes())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
129
src/main.rs
Normal file
129
src/main.rs
Normal file
@ -0,0 +1,129 @@
|
||||
extern crate base64;
|
||||
extern crate byteorder;
|
||||
extern crate hex;
|
||||
extern crate rand;
|
||||
extern crate serde_json;
|
||||
extern crate signal_hook;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write, Result};
|
||||
use std::os::unix::net::{UnixStream, UnixListener};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use byteorder::{BE, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
mod bridge;
|
||||
mod key;
|
||||
mod list;
|
||||
mod sign;
|
||||
mod startup;
|
||||
mod store;
|
||||
|
||||
// defined in the specification document
|
||||
const SSH_AGENTC_REQUEST_IDENTITIES: u8 = 11;
|
||||
const SSH_AGENTC_SIGN_REQUEST: u8 = 13;
|
||||
const SSH_AGENT_FAILURE: u8 = 5;
|
||||
|
||||
type KeyStore = HashMap<Vec<u8>, key::Key>;
|
||||
|
||||
fn main() {
|
||||
// check if "-h" is passed, print help and exit if so
|
||||
let help_requested = startup::print_help();
|
||||
if help_requested { return; }
|
||||
|
||||
let foreground_mode = startup::is_foreground();
|
||||
let socket = startup::get_socket();
|
||||
|
||||
let pid = if foreground_mode {
|
||||
// if we are running in the foreground mode, set up the
|
||||
// socket, and use our own PID
|
||||
startup::initialize_socket(&socket);
|
||||
startup::get_pid()
|
||||
} else {
|
||||
// if not, spawn a child that runs in foreground mode,
|
||||
// and use its PID
|
||||
startup::spawn_child(&socket)
|
||||
.expect("Could not execute a child process.")
|
||||
};
|
||||
|
||||
// print the greetings: the socket and PID information
|
||||
println!("{}", startup::create_shell_commands(&socket, pid));
|
||||
// child will take over if we are not in foreground mode
|
||||
if !foreground_mode { return; }
|
||||
|
||||
// keystore cache that will be used throughout the application
|
||||
let store = Arc::new(Mutex::new(KeyStore::new()));
|
||||
|
||||
let listener = UnixListener::bind(socket).expect("Could not create the socket.");
|
||||
|
||||
for stream in listener.incoming() {
|
||||
if let Ok(mut stream) = stream {
|
||||
let store = Arc::clone(&store);
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
if let Err(_) = handle_stream(&store, &mut stream) {
|
||||
break;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_stream(store: &Arc<Mutex<KeyStore>>, stream: &mut UnixStream) -> Result<()> {
|
||||
let length = stream.read_u32::<BE>()?;
|
||||
let msg_type = stream.read_u8()?;
|
||||
match msg_type {
|
||||
SSH_AGENTC_REQUEST_IDENTITIES => {
|
||||
let mut store = store.lock().unwrap();
|
||||
|
||||
// grab the keys from termux-api
|
||||
let json = bridge::list_keys();
|
||||
|
||||
// parse the JSON to refresh the local cache
|
||||
store::load_all(&mut store, json);
|
||||
|
||||
// print the keys from the cache
|
||||
list::write_list_response(&store, stream)?;
|
||||
},
|
||||
SSH_AGENTC_SIGN_REQUEST => {
|
||||
let mut store = store.lock().unwrap();
|
||||
|
||||
// create a separate stream for reading
|
||||
let mut read = stream.try_clone().unwrap().take((length - 1).into());
|
||||
|
||||
// parse the request received from the client
|
||||
let request = match sign::read_request(&store, &mut read)? {
|
||||
Some(request) => request,
|
||||
_ => { return write_error(stream) },
|
||||
};
|
||||
|
||||
// transmit the request to termux-api
|
||||
let signature = bridge::sign(
|
||||
&request.key().alias, &request.keystore_name(), &request.data()
|
||||
);
|
||||
|
||||
if signature.len() == 0 {
|
||||
// termux-api returned empty response
|
||||
// it is probably locked due to user validity enforcement
|
||||
return write_error(stream);
|
||||
}
|
||||
|
||||
// print the signature response to the client
|
||||
sign::write_response(&request, &signature, stream)?;
|
||||
stream.flush()?;
|
||||
},
|
||||
_ => {
|
||||
// unsupported message type
|
||||
write_error(stream)?;
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_error(stream: &mut Write) -> Result<()> {
|
||||
stream.write_u32::<BE>(1)?;
|
||||
stream.write_u8(SSH_AGENT_FAILURE)?;
|
||||
stream.flush()?;
|
||||
Ok(())
|
||||
}
|
52
src/sign/mod.rs
Normal file
52
src/sign/mod.rs
Normal file
@ -0,0 +1,52 @@
|
||||
mod request;
|
||||
mod response;
|
||||
|
||||
use std::io::{Read, Write, Result};
|
||||
use byteorder::{BE, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
use super::KeyStore;
|
||||
use super::key::Algorithm;
|
||||
use self::request::SignRequest;
|
||||
|
||||
/// Parses a request to sign some data, and returns a struct
|
||||
/// that represents this request. Returns None if the
|
||||
/// corresponding key was not found in the keystore.
|
||||
pub fn read_request<'a>(store: &'a KeyStore, stream: &mut Read) -> Result<Option<SignRequest<'a>>> {
|
||||
// total length and message type is already read by the main function
|
||||
// next up, blob length
|
||||
let blob_length = stream.read_u32::<BE>()?;
|
||||
let mut blob: Vec<u8> = Vec::new();
|
||||
blob.write_u32::<BE>(blob_length)?;
|
||||
stream.take(blob_length.into()).read_to_end(&mut blob)?;
|
||||
|
||||
// data to be signed
|
||||
let data_length = stream.read_u32::<BE>()?;
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
stream.take(data_length.into()).read_to_end(&mut data)?;
|
||||
|
||||
// finally, flags
|
||||
let flags = stream.read_u32::<BE>()?;
|
||||
|
||||
// while SSH agent protocol relies on blobs heavily
|
||||
// it is easier to work with key structs in our context
|
||||
if let Some(key) = store.get(blob.as_slice()) {
|
||||
Ok(Some(SignRequest::new(key, data, flags)))
|
||||
} else {
|
||||
// key not found
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outputs a sign response given a key and a signature received from `termux-api`.
|
||||
pub fn write_response(request: &SignRequest, signature: &[u8], stream: &mut Write) -> Result<()> {
|
||||
let name = request.ssh_name(); // name to use in the header
|
||||
match &request.key().algorithm {
|
||||
Algorithm::Rsa => {
|
||||
response::write_rsa_response(name, signature, stream)
|
||||
},
|
||||
Algorithm::Ec(_) => {
|
||||
// curve is already in name
|
||||
response::write_ec_response(name, signature, stream)
|
||||
},
|
||||
}
|
||||
}
|
74
src/sign/request.rs
Normal file
74
src/sign/request.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use ::key::{Key, Algorithm, Curve};
|
||||
|
||||
// defined in the specification document
|
||||
const SSH_AGENT_RSA_SHA2_256: u32 = 2;
|
||||
const SSH_AGENT_RSA_SHA2_512: u32 = 4;
|
||||
|
||||
/// Represents a request for signing send by a client.
|
||||
pub struct SignRequest<'a> {
|
||||
key: &'a Key,
|
||||
data: Vec<u8>,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
impl<'a> SignRequest<'a> {
|
||||
pub fn new(key: &Key, data: Vec<u8>, flags: u32) -> SignRequest {
|
||||
SignRequest { key, data, flags }
|
||||
}
|
||||
|
||||
pub fn key(&self) -> &Key {
|
||||
&self.key
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
// TODO: these two functions below are too similiar...
|
||||
|
||||
/// Returns the name to be used when communicating with the
|
||||
/// Android keystore.
|
||||
pub fn keystore_name(&self) -> &'static str {
|
||||
// TODO: handle invalid flags gracefully
|
||||
match self.key.algorithm {
|
||||
Algorithm::Rsa => {
|
||||
match self.flags {
|
||||
0 => "SHA1withRSA",
|
||||
SSH_AGENT_RSA_SHA2_256 => "SHA256withRSA",
|
||||
SSH_AGENT_RSA_SHA2_512 => "SHA512withRSA",
|
||||
f => panic!("Unknown flag {}", f),
|
||||
}
|
||||
},
|
||||
Algorithm::Ec(ref curve) => {
|
||||
match curve {
|
||||
Curve::P256 => "SHA256withECDSA",
|
||||
Curve::P384 => "SHA386withECDSA",
|
||||
Curve::P521 => "SHA512withECDSA",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the name to be used when communicating with an
|
||||
/// ssh-agent client.
|
||||
pub fn ssh_name(&self) -> &'static str {
|
||||
// TODO: handle invalid flags gracefully
|
||||
match self.key.algorithm {
|
||||
Algorithm::Rsa => {
|
||||
match self.flags {
|
||||
0 => "ssh-rsa",
|
||||
SSH_AGENT_RSA_SHA2_256 => "rsa-sha2-256",
|
||||
SSH_AGENT_RSA_SHA2_512 => "rsa-sha2-512",
|
||||
f => panic!("Unknown flag {}", f),
|
||||
}
|
||||
},
|
||||
Algorithm::Ec(ref curve) => {
|
||||
match curve {
|
||||
Curve::P256 => "ecdsa-sha2-nistp256",
|
||||
Curve::P384 => "ecdsa-sha2-nistp384",
|
||||
Curve::P521 => "ecdsa-sha2-nistp521",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
74
src/sign/response.rs
Normal file
74
src/sign/response.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use std::io::{Write, Result};
|
||||
use byteorder::{BE, WriteBytesExt};
|
||||
|
||||
// defined in the specification document
|
||||
const SSH_AGENT_SIGN_RESPONSE: u8 = 14;
|
||||
|
||||
/// Write a RSA sign response to a given stream.
|
||||
/// Name should contain the hash algorithm used to sign the data
|
||||
/// (e.g. "rsa-sha2-512").
|
||||
pub fn write_rsa_response(name: &str, signature: &[u8], stream: &mut Write) -> Result<()> {
|
||||
// the RSA signature received from Android keystore only
|
||||
// contains the signature, no parsing is necessary
|
||||
|
||||
let length = signature.len() as u32;
|
||||
|
||||
// total length
|
||||
stream.write_u32::<BE>(length + 25)?;
|
||||
// message type
|
||||
stream.write_u8(SSH_AGENT_SIGN_RESPONSE)?;
|
||||
// size for the rest of the packet
|
||||
stream.write_u32::<BE>(length + 20)?;
|
||||
// signature type
|
||||
stream.write_u32::<BE>(12)?;
|
||||
stream.write_all(name.as_bytes())?;
|
||||
// signature
|
||||
stream.write_u32::<BE>(length)?;
|
||||
stream.write_all(&signature)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write an EC sign response to a given stream.
|
||||
/// Name should contain the hash algorithm associate with
|
||||
/// the key size (e.g. "ecdsa-sha2-nistp256")
|
||||
pub fn write_ec_response(name: &str, signature: &[u8], stream: &mut Write) -> Result<()> {
|
||||
// the EC signature received from Android keystore is formatted
|
||||
// in ASN.1 DER format.
|
||||
// TODO: replace these crude calculations with a proper DER parser
|
||||
// first byte is always 0x30
|
||||
assert_eq!(0x30, signature[0]);
|
||||
// if the second byte is 0x81, we have a longer response (for P-521)
|
||||
let r_start = if signature[1] == 0x81 { 3 } else { 2 };
|
||||
// 0x02 means integer
|
||||
assert_eq!(0x02, signature[r_start]);
|
||||
// the next byte is the length of the integer
|
||||
let r_length = signature[r_start+1] as usize;
|
||||
// repeat for the other value
|
||||
let s_start = r_start+2+r_length;
|
||||
assert_eq!(0x02, signature[s_start]);
|
||||
let s_length = signature[s_start+1] as usize;
|
||||
|
||||
// acquire the values
|
||||
let r = &signature[r_start+2..s_start];
|
||||
let s = &signature[s_start+2..s_start+2+s_length];
|
||||
|
||||
let rs_length = (r_length + s_length) as u32;
|
||||
|
||||
// total packet size
|
||||
stream.write_u32::<BE>(rs_length + 40)?;
|
||||
// message type
|
||||
stream.write_u8(SSH_AGENT_SIGN_RESPONSE)?;
|
||||
// size for the rest of the packet
|
||||
stream.write_u32::<BE>(rs_length + 35)?;
|
||||
// signature type
|
||||
stream.write_u32::<BE>(19)?;
|
||||
stream.write_all(name.as_bytes())?;
|
||||
// total size of values
|
||||
stream.write_u32::<BE>(rs_length + 8)?;
|
||||
// values
|
||||
stream.write_u32::<BE>(r_length as u32)?;
|
||||
stream.write_all(r)?;
|
||||
stream.write_u32::<BE>(s_length as u32)?;
|
||||
stream.write_all(s)?;
|
||||
Ok(())
|
||||
}
|
122
src/startup.rs
Normal file
122
src/startup.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use signal_hook;
|
||||
|
||||
/// Root folder for the created socket.
|
||||
/// A subfolder will be created inside this folder.
|
||||
const SOCKET_FOLDER: &str = "/data/data/com.termux/files/usr/tmp/";
|
||||
|
||||
/// Returns true if the application was started in
|
||||
/// the foreground mode.
|
||||
pub fn is_foreground() -> bool {
|
||||
env::args().any(|a| a == "-D")
|
||||
}
|
||||
|
||||
/// Get the socket path to use.
|
||||
/// If available, the path given with the "-a" parameter is used.
|
||||
/// Otherwise a new path is generated.
|
||||
pub fn get_socket() -> PathBuf {
|
||||
// check if a path is given in the command line
|
||||
let mut args = env::args();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "-a" {
|
||||
let path = args.next().expect("-a argument requires a path");
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
};
|
||||
|
||||
// generate a new path
|
||||
let rand_string: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(12)
|
||||
.collect();
|
||||
|
||||
let mut path = PathBuf::from(SOCKET_FOLDER);
|
||||
path.push(format!("ssh-{}", rand_string));
|
||||
path.push(format!("agent.{}", process::id()));
|
||||
path
|
||||
}
|
||||
|
||||
/// Initializes the socket by creating the parent folder,
|
||||
/// and setting up triggers to cleanup the socket and the folder
|
||||
/// when the application exists.
|
||||
pub fn initialize_socket(socket: &Path) {
|
||||
// TODO: this function will fail with user-provided socket paths
|
||||
|
||||
socket.parent().and_then(|f| fs::create_dir(f).ok())
|
||||
.expect("Could not create the parent folder for the socket");
|
||||
|
||||
register_cleanup(signal_hook::SIGINT, socket);
|
||||
register_cleanup(signal_hook::SIGTERM, socket);
|
||||
}
|
||||
|
||||
/// Register a signal hook to remove the folder and the socket.
|
||||
fn register_cleanup(signal: i32, socket: &Path) {
|
||||
let socket = socket.to_owned();
|
||||
let folder = socket.parent().unwrap().to_owned();
|
||||
|
||||
unsafe {
|
||||
signal_hook::register(signal, move || {
|
||||
fs::remove_file(&socket).unwrap();
|
||||
fs::remove_dir(&folder).unwrap();
|
||||
process::exit(0);
|
||||
}).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the PID of this process.
|
||||
pub fn get_pid() -> u32 { process::id() }
|
||||
|
||||
/// Executes a child process, with the appropriate parameters
|
||||
/// passed so that the process reuses the same socket path,
|
||||
/// and runs in foreground mode. Returns None in case of an error.
|
||||
pub fn spawn_child(socket: &Path) -> Option<u32> {
|
||||
let socket = socket.to_str()?;
|
||||
let command = env::args().next()?;
|
||||
let child = Command::new(command)
|
||||
.args(&["-a", socket, "-D"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.spawn().ok()?;
|
||||
Some(child.id())
|
||||
}
|
||||
|
||||
/// Builds commands that should be executed in the user's shell
|
||||
/// so that the socket path and agent PID is easily accessible.
|
||||
/// If the "-c" is passed, c-shell style commands are generated,
|
||||
/// if not, bash commands are generated.
|
||||
pub fn create_shell_commands(socket: &Path, pid: u32) -> String {
|
||||
let socket = socket.to_str().expect("Socket path is invalid.");
|
||||
let c_style = env::args().any(|a| a == "-c");
|
||||
if c_style {
|
||||
format!("setenv SSH_AUTH_SOCK {};
|
||||
setenv SSH_AGENT_PID {};
|
||||
echo Agent pid {};", socket, pid, pid)
|
||||
} else {
|
||||
format!("SSH_AUTH_SOCK={}; export SSH_AUTH_SOCK;
|
||||
SSH_AGENT_PID={}; export SSH_AGENT_PID;
|
||||
echo Agent pid {};", socket, pid, pid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints the short help documentation to the console if one of the
|
||||
/// arguments is "-h". Returns true if this was the case and the help
|
||||
/// was printed, false otherwise.
|
||||
pub fn print_help() -> bool {
|
||||
if env::args().any(|a| a == "-h") {
|
||||
println!("usage: tergent [-c] [-D] [-a bind_address]
|
||||
Options:
|
||||
-c Print C-shell style commands.
|
||||
-D Foreground mode, do not fork to background.
|
||||
-a Use the given socket address instead of a
|
||||
randomly generated one.");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
64
src/store/blob.rs
Normal file
64
src/store/blob.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use std::io::{Write, Result};
|
||||
use byteorder::{BE, WriteBytesExt};
|
||||
|
||||
use ::key::Curve;
|
||||
|
||||
const EC_UNCOMPRESSED_POINT: u8 = 4;
|
||||
// compressed points are not supported
|
||||
|
||||
/// Create a RSA key blob with the given modulus.
|
||||
/// Note that the exponent is hardcoded as 65537 for now.
|
||||
pub fn from_rsa(modulus: Vec<u8>) -> Result<Vec<u8>> {
|
||||
let length = modulus.len() as u32;
|
||||
|
||||
let mut blob = Vec::new();
|
||||
// blob total size
|
||||
blob.write_u32::<BE>(length + 23)?;
|
||||
// header
|
||||
blob.write_u32::<BE>(7)?;
|
||||
blob.write_all("ssh-rsa".as_bytes())?;
|
||||
// exponent
|
||||
blob.write_u32::<BE>(3)?;
|
||||
blob.write_all(&[1, 0, 1])?;
|
||||
// modulus
|
||||
blob.write_u32::<BE>(length + 1)?;
|
||||
blob.write_u8(0)?; // modulus is positive (see https://crypto.stackexchange.com/q/30608)
|
||||
blob.write_all(&modulus)?;
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
/// Create an EC key blob with the given parameters.
|
||||
pub fn from_ec(curve: &Curve, x: Vec<u8>, y: Vec<u8>) -> Result<Vec<u8>> {
|
||||
// both parameters must have these lengths
|
||||
let xy_length = match curve {
|
||||
Curve::P256 => 32,
|
||||
Curve::P384 => 48,
|
||||
Curve::P521 => 66,
|
||||
};
|
||||
// will need to prepend with zeroes to make sure
|
||||
// the parameters have these sizes
|
||||
let x_padding = xy_length as usize - x.len();
|
||||
let y_padding = xy_length as usize - y.len();
|
||||
|
||||
// curve name, e.g. "nistp256"
|
||||
let curve = curve.name().as_bytes();
|
||||
|
||||
let mut blob = Vec::new();
|
||||
// blob total size
|
||||
blob.write_u32::<BE>(xy_length * 2 + 40)?;
|
||||
// header, algorithm name
|
||||
blob.write_u32::<BE>(19)?;
|
||||
blob.write_all("ecdsa-sha2-".as_bytes())?;
|
||||
blob.write_all(curve)?;
|
||||
// curve name
|
||||
blob.write_u32::<BE>(8)?;
|
||||
blob.write_all(curve)?;
|
||||
// parameters
|
||||
blob.write_u32::<BE>(xy_length * 2 + 1)?;
|
||||
blob.write_u8(EC_UNCOMPRESSED_POINT)?;
|
||||
for _ in 0..x_padding { blob.write_u8(0)?; };
|
||||
blob.write_all(&x)?;
|
||||
for _ in 0..y_padding { blob.write_u8(0)?; };
|
||||
blob.write_all(&y)?;
|
||||
Ok(blob)
|
||||
}
|
72
src/store/mod.rs
Normal file
72
src/store/mod.rs
Normal file
@ -0,0 +1,72 @@
|
||||
mod blob;
|
||||
|
||||
use hex;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::KeyStore;
|
||||
use super::key;
|
||||
use super::key::{Key, Algorithm};
|
||||
|
||||
/// Removes all the keys from the store, and fills it back
|
||||
/// using the data contained in the JSON string.
|
||||
///
|
||||
/// The input should ideally come from `termux-api`.
|
||||
pub fn load_all(store: &mut KeyStore, json: String) {
|
||||
// expecting a well-formed JSON with an array at the root
|
||||
let keys = serde_json::from_str::<Value>(&json).ok()
|
||||
.and_then(|v| if let Value::Array(vec) = v { Some(vec) } else { None })
|
||||
.expect("Cannot read the JSON input.");
|
||||
|
||||
// panic if any of the objects inside the array is malformed
|
||||
let keys = keys.iter()
|
||||
.map(|k| parse_key(k).expect("Cannot read one of the keys."));
|
||||
|
||||
store.clear();
|
||||
store.extend(keys);
|
||||
}
|
||||
|
||||
/// Parse a single JSON object containing information about a key.
|
||||
fn parse_key(object: &Value) -> Option<(Vec<u8>, key::Key)> {
|
||||
let alias = object.get("alias")?.as_str()?;
|
||||
let algorithm = object.get("algorithm")?.as_str()?;
|
||||
let size = object.get("size")?.as_u64()?;
|
||||
|
||||
let key = Key {
|
||||
algorithm: Algorithm::parse(algorithm, size)?,
|
||||
alias: String::from(alias),
|
||||
};
|
||||
|
||||
let blob = match key.algorithm {
|
||||
Algorithm::Rsa => {
|
||||
let modulus = object.get("modulus")?.as_hex()?;
|
||||
blob::from_rsa(modulus)
|
||||
},
|
||||
Algorithm::Ec(ref curve) => {
|
||||
let x = object.get("x")?.as_hex()?;
|
||||
let y = object.get("y")?.as_hex()?;
|
||||
blob::from_ec(curve, x, y)
|
||||
},
|
||||
}.ok()?;
|
||||
|
||||
Some((blob, key))
|
||||
}
|
||||
|
||||
trait HexValue {
|
||||
fn as_hex(&self) -> Option<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// Extend the JSON value struct to support parsing base16 integers.
|
||||
impl HexValue for Value {
|
||||
fn as_hex(&self) -> Option<Vec<u8>> {
|
||||
let value = self.as_str()?;
|
||||
|
||||
if value.len() % 2 == 1 {
|
||||
// hex module will fail if the number of letters is odd
|
||||
// prepend a zero to get the result without failing
|
||||
hex::decode(format!("0{}", value)).ok()
|
||||
} else {
|
||||
hex::decode(value).ok()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user