Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
blockloop committed Apr 1, 2023
0 parents commit 68545d6
Show file tree
Hide file tree
Showing 6 changed files with 1,334 additions and 0 deletions.
123 changes: 123 additions & 0 deletions README.md
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.
124 changes: 124 additions & 0 deletions agent.py
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)
Loading

0 comments on commit 68545d6

Please sign in to comment.