-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 68545d6
Showing
6 changed files
with
1,334 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
# Passwordstore SSH IdentityAgent | ||
|
||
‼️ This project was writen entirely by ChatGPT ‼️ | ||
|
||
That includes this README (except for this part), the entirety of `agent.py`, and the systemd files. | ||
|
||
I have been actively using this agent on my personal and work machines since it was created. I would like to add support for more key types eventually and move the key configuration to a yaml file, but I haven't had the urge to do so since it's working perfectly fine. | ||
|
||
Below this line is the original README written by ChatGPT. I wrote the systemd section. | ||
|
||
--- | ||
|
||
This script provides an implementation of a custom SSH agent that retrieves private keys and their corresponding passphrases from [pass](https://www.passwordstore.org/), the standard Unix password manager. The script supports Ed25519 keys, listens on a Unix domain socket, and uses the Paramiko library for SSH key handling. | ||
|
||
## Dependencies | ||
|
||
- Python 3.6 or higher | ||
- [Paramiko](https://www.paramiko.org/) library | ||
|
||
To install Paramiko, run: | ||
|
||
```bash | ||
pip install paramiko | ||
``` | ||
|
||
## Usage | ||
|
||
1. Edit the `KEYS` list in the script to include your key details: | ||
|
||
```python | ||
KEYS = [ | ||
{ | ||
"type": "ed25519", | ||
"key_path": "your-key-path", | ||
"pass_key": "your-pass-key", | ||
"store_location": "/path/to/your/store" | ||
} | ||
] | ||
``` | ||
|
||
2. Replace `your-key-path`, `your-pass-key`, and `/path/to/your/store` with the appropriate values for your key. | ||
|
||
3. Run the script with the desired socket path as the command line argument: | ||
|
||
```bash | ||
python custom_ssh_agent.py <socket_path> | ||
``` | ||
|
||
Replace `<socket_path>` with the desired path for the Unix domain socket. | ||
|
||
4. Connect to the custom SSH agent using your SSH client by setting the `SSH_AUTH_SOCK` environment variable to the path of the Unix domain socket: | ||
|
||
```bash | ||
export SSH_AUTH_SOCK=<socket_path> | ||
``` | ||
|
||
5. Your SSH client should now use the custom SSH agent for authentication. | ||
|
||
## Example | ||
|
||
Suppose you have an Ed25519 key with the following details: | ||
|
||
- Key path in pass: `keys/ssh/id_ed25519` | ||
- Passphrase path in pass: `keys/ssh/id_ed25519_passphrase` | ||
- Password store location: `/home/user/.password-store` | ||
|
||
Edit the `KEYS` list as follows: | ||
|
||
```python | ||
KEYS = [ | ||
{ | ||
"type": "ed25519", | ||
"key_path": "keys/ssh/id_ed25519", | ||
"pass_key": "keys/ssh/id_ed25519_passphrase", | ||
"store_location": "/home/user/.password-store" | ||
} | ||
] | ||
``` | ||
|
||
Run the script: | ||
|
||
```bash | ||
python custom_ssh_agent.py /tmp/custom_ssh_agent.sock | ||
``` | ||
|
||
Set the `SSH_AUTH_SOCK` environment variable: | ||
|
||
```bash | ||
export SSH_AUTH_SOCK=/tmp/custom_ssh_agent.sock | ||
``` | ||
|
||
Now, your SSH client will use the custom SSH agent for authentication. | ||
|
||
|
||
## Make it automatic with systemd | ||
|
||
```bash | ||
# copy the agent to your local bin dir | ||
cp agent.py $HOME/.local/bin/ssh-pass-agent.py | ||
|
||
# IMPORANT !! | ||
# make sure you modify the KEY variable in $HOME/.local/bin/ssh-pass-agent.py to | ||
# reflect your pass key information | ||
|
||
# copy the files to systemd's config location | ||
cp ./systemd/ssh-pass-agent.socket ~/.config/systemd/user/ | ||
cp ./systemd/ssh-pass-agent.service ~/.config/systemd/user/ | ||
|
||
# reload systemd daemon | ||
systemctl --user daemon-reload | ||
|
||
# start the socket and the service for good measure and enable them to start at boot | ||
systemctl --user enable --now ssh-pass-agent.socket | ||
systemctl --user enable --now ssh-pass-agent.service | ||
|
||
# Tell SSH to use the socket | ||
# (you might want to put this into your ~/.bashrc) | ||
export SSH_AUTH_SOCK=/run/user/$(id -u)/ssh-pass-agent.sock | ||
``` | ||
|
||
Now when you invoke `ssh <host>` the socket will be activated, the service will | ||
be started and the only password prompt you should see is for the gpg | ||
passphrase which unlocks the password store. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import os | ||
import socket | ||
import struct | ||
import sys | ||
from paramiko.message import Message | ||
from paramiko.agent import Agent | ||
from paramiko.ed25519key import Ed25519Key | ||
from paramiko.ssh_exception import SSHException | ||
import subprocess | ||
import io | ||
|
||
class CustomSSHAgent(Agent): | ||
AGENTC_REQUEST_IDENTITIES = bytes([11]) | ||
AGENTC_SIGN_REQUEST = bytes([13]) | ||
AGENT_IDENTITIES_ANSWER = bytes([12]) | ||
AGENT_SIGN_RESPONSE = bytes([14]) | ||
|
||
def __init__(self, keys): | ||
self.keys = [] | ||
for key in keys: | ||
try: | ||
if key["type"].lower() == "ed25519": | ||
passphrase = self.get_passphrase(key["pass_key"], key["store_location"]) | ||
private_key_str = self.get_private_key(key["key_path"]) | ||
private_key_file = io.StringIO(private_key_str) | ||
self.keys.append(Ed25519Key.from_private_key(private_key_file, password=passphrase)) | ||
except SSHException as e: | ||
print(f"Error loading key from {key['key_path']}: {e}") | ||
|
||
def get_passphrase(self, pass_key, store_location): | ||
env = os.environ.copy() | ||
env['PASSWORD_STORE_DIR'] = store_location | ||
|
||
result = subprocess.run(['pass', pass_key], capture_output=True, text=True, env=env) | ||
if result.returncode != 0: | ||
print(f"Error retrieving passphrase for key {pass_key}: {result.stderr}") | ||
return None | ||
return result.stdout.strip() | ||
|
||
def get_private_key(self, key_path): | ||
result = subprocess.run(['pass', key_path], capture_output=True, text=True) | ||
if result.returncode != 0: | ||
print(f"Error retrieving private key for key_path {key_path}: {result.stderr}") | ||
return None | ||
return result.stdout.strip() | ||
|
||
def handle_message(self, msg): | ||
m = Message(msg) | ||
cmd = m.get_byte() | ||
|
||
if cmd == CustomSSHAgent.AGENTC_REQUEST_IDENTITIES: | ||
resp = Message() | ||
resp.add_byte(CustomSSHAgent.AGENT_IDENTITIES_ANSWER) | ||
resp.add_int(len(self.keys)) | ||
for key in self.keys: | ||
resp.add_string(key.asbytes()) | ||
resp.add_string(f"{key.get_name()} key loaded from {key}") | ||
return resp.asbytes() | ||
|
||
if cmd == CustomSSHAgent.AGENTC_SIGN_REQUEST: | ||
key_blob = m.get_string() | ||
data = m.get_string() | ||
flags = m.get_int() | ||
|
||
resp = Message() | ||
resp.add_byte(CustomSSHAgent.AGENT_SIGN_RESPONSE) | ||
for key in self.keys: | ||
if key_blob == key.asbytes(): | ||
sig = key.sign_ssh_data(data) | ||
resp.add_string(sig) | ||
return resp.asbytes() | ||
|
||
failure_response = Message() | ||
failure_response.add_byte(bytes([255])) # Custom failure code, converted to bytes | ||
return failure_response.asbytes() | ||
|
||
def handle_client(agent, conn, addr): | ||
try: | ||
while True: | ||
msg = conn.recv(4) | ||
if len(msg) == 0: | ||
break | ||
msg_len = struct.unpack(">I", msg)[0] | ||
msg = conn.recv(msg_len) | ||
response = agent.handle_message(msg) | ||
conn.send(struct.pack(">I", len(response)) + response) | ||
finally: | ||
conn.close() | ||
|
||
|
||
KEYS = [ | ||
{ | ||
"type": "ed25519", | ||
"key_path": "your-key-path", | ||
"pass_key": "your-pass-key", | ||
"store_location": "/path/to/your/store" | ||
} | ||
] | ||
|
||
if __name__ == "__main__": | ||
agent = CustomSSHAgent(KEYS) | ||
|
||
if len(sys.argv) != 2: | ||
print(f"Usage: {sys.argv[0]} <socket_path>") | ||
sys.exit(1) | ||
|
||
socket_path = sys.argv[1] | ||
|
||
if os.path.exists(socket_path): | ||
os.unlink(socket_path) | ||
|
||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
sock.bind(socket_path) | ||
os.chmod(socket_path, 0o600) | ||
|
||
try: | ||
sock.listen(1) | ||
|
||
while True: | ||
conn, addr = sock.accept() | ||
handle_client(agent, conn, addr) | ||
finally: | ||
sock.close() | ||
os.unlink(socket_path) |
Oops, something went wrong.