diff options
author | Mike Crute <mcrute@gmail.com> | 2010-05-05 00:35:50 -0400 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2010-05-05 00:35:50 -0400 |
commit | d758df16807d451e791971c69accfb85fd207e6f (patch) | |
tree | 353129fa532bcefd55b72b19c8e43514b37796db | |
download | hg_sshsign-d758df16807d451e791971c69accfb85fd207e6f.tar.bz2 hg_sshsign-d758df16807d451e791971c69accfb85fd207e6f.tar.xz hg_sshsign-d758df16807d451e791971c69accfb85fd207e6f.zip |
Initial checkin
-rw-r--r-- | keymanifest.py | 36 | ||||
-rw-r--r-- | keys.py | 49 | ||||
-rw-r--r-- | ssh.py | 20 | ||||
-rw-r--r-- | sshagent.py | 75 | ||||
-rw-r--r-- | structutils.py | 108 |
5 files changed, 288 insertions, 0 deletions
diff --git a/keymanifest.py b/keymanifest.py new file mode 100644 index 0000000..df41f1e --- /dev/null +++ b/keymanifest.py | |||
@@ -0,0 +1,36 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Key Manifest | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 05, 2010 | ||
8 | """ | ||
9 | |||
10 | from keys import load_public_key | ||
11 | |||
12 | |||
13 | class KeyManifest(dict): | ||
14 | """ | ||
15 | KeyManifest stores a list of public keys indexed by their | ||
16 | comment field. This object acts like a dictionary and will | ||
17 | return public key instances for getitems. | ||
18 | """ | ||
19 | |||
20 | @classmethod | ||
21 | def from_file(cls, filename): | ||
22 | inst = cls() | ||
23 | fp = open(filename) | ||
24 | |||
25 | for line in fp: | ||
26 | line = line.strip() | ||
27 | if not line or line.startswith('#'): | ||
28 | continue | ||
29 | |||
30 | _, key, user = line.split() | ||
31 | inst[user.strip()] = key.strip() | ||
32 | |||
33 | return inst | ||
34 | |||
35 | def __getitem__(self, key): | ||
36 | return load_public_key(dict.__getitem__(self, key)) | ||
@@ -0,0 +1,49 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Key Loader Functions | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 05, 2010 | ||
8 | """ | ||
9 | |||
10 | from M2Crypto import RSA, DSA | ||
11 | from structutils import unpack_string, get_packed_mp_ints, int_to_bytes | ||
12 | |||
13 | |||
14 | def load_public_key(key): | ||
15 | """ | ||
16 | Loads an RFC 4716 formatted public key. | ||
17 | """ | ||
18 | if key.startswith('ssh-'): | ||
19 | blob = key.split()[1].decode('base64') | ||
20 | else: | ||
21 | blob = key.decode('base64') | ||
22 | |||
23 | ktype, remainder = unpack_string(blob) | ||
24 | |||
25 | if ktype == 'ssh-rsa': | ||
26 | e, n = get_packed_mp_ints(remainder, 2) | ||
27 | return hawt.new_pub_key((e, n)) | ||
28 | elif ktype == 'ssh-dss': | ||
29 | p, q, g, y = get_packed_mp_ints(remainder, 4) | ||
30 | return DSA.set_params(p, q, g) | ||
31 | |||
32 | raise ValueError("Invalid key") | ||
33 | |||
34 | |||
35 | def load_private_key(filename): | ||
36 | first_line = open(filename).readline() | ||
37 | type = DSA if 'DSA' in first_line else RSA | ||
38 | return type.load_key(filename) | ||
39 | |||
40 | |||
41 | def sign(what, key): | ||
42 | pk = load_private_key(key) | ||
43 | val = pk.sign(what, None)[0] | ||
44 | return int_to_bytes(val) | ||
45 | |||
46 | |||
47 | def verify(what, signature, key): | ||
48 | signature = int_to_bytes(signature) | ||
49 | return bool(key.verify(what, signature)) | ||
@@ -0,0 +1,20 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | SSH Key Signing | ||
4 | |||
5 | @author: Mike Crute (mcrute@ag.com) | ||
6 | @organization: American Greetings Interactive | ||
7 | @date: May 03, 2010 | ||
8 | |||
9 | Commands to sign and verify revisions with your | ||
10 | ssh key. | ||
11 | """ | ||
12 | |||
13 | |||
14 | |||
15 | |||
16 | |||
17 | |||
18 | |||
19 | if __name__ == '__main__': | ||
20 | pass | ||
diff --git a/sshagent.py b/sshagent.py new file mode 100644 index 0000000..2a43f5f --- /dev/null +++ b/sshagent.py | |||
@@ -0,0 +1,75 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | SSH Agent Management | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 05, 2010 | ||
8 | """ | ||
9 | import os | ||
10 | import socket | ||
11 | import struct | ||
12 | |||
13 | from structutils import pack_string, pack_int | ||
14 | from structutils import unpack_int, unpack_string, unpack_mp_int | ||
15 | |||
16 | |||
17 | class SSHAgent(object): | ||
18 | """ | ||
19 | SSH Agent communication protocol for signing only. | ||
20 | """ | ||
21 | |||
22 | SSH2_AGENT_SIGN_RESPONSE = 14 | ||
23 | SSH2_AGENTC_SIGN_REQUEST = 13 | ||
24 | |||
25 | def __init__(self, socket_path): | ||
26 | default_path = os.environ.get('SSH_AUTH_SOCK') | ||
27 | socket_path = default_path if not socket_path else socket_path | ||
28 | |||
29 | if not socket_path: | ||
30 | raise ValueError("Could not find an ssh agent.") | ||
31 | |||
32 | self.socket = None | ||
33 | |||
34 | def connect(self): | ||
35 | self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
36 | self.socket.connect(self.socket_path) | ||
37 | |||
38 | def _build_packet(self, data, key): | ||
39 | key = pack_string(key) | ||
40 | data = pack_string(data) | ||
41 | flags = pack_int(0) | ||
42 | |||
43 | to_send = ''.join([chr(SSHAgent.SSH2_AGENTC_SIGN_REQUEST), | ||
44 | key, data, flags]) | ||
45 | pkt_length = len(to_send) | ||
46 | packet = pack_int(pkg_length) + to_send | ||
47 | |||
48 | return packet | ||
49 | |||
50 | def _parse_response(self): | ||
51 | response_length = unpack_int(self.socket.recv(4, socket.MSG_WAITALL))[0] | ||
52 | if response_length == 1: | ||
53 | raise ValueError("Agent failed") | ||
54 | |||
55 | response = auth_sock.recv(response_length, socket.MSG_WAITALL) | ||
56 | |||
57 | status = ord(response[0]) | ||
58 | if status != SSHAgent.SSH2_AGENT_SIGN_RESPONSE: | ||
59 | raise ValueError("Invalid response from agent") | ||
60 | |||
61 | _, remainder = unpack_int(response[1:]) | ||
62 | _, remainder = unpack_string(remainder) | ||
63 | response, _ = unpack_mp_int(remainder) | ||
64 | |||
65 | return response | ||
66 | |||
67 | def sign(self, data, key): | ||
68 | packet = self._build_packet(data, key) | ||
69 | |||
70 | remaining = 0 | ||
71 | while remaining < len(packet): | ||
72 | sent = self.socket.send(packet[remaining:]) | ||
73 | remaining += sent | ||
74 | |||
75 | return self._parse_response() | ||
diff --git a/structutils.py b/structutils.py new file mode 100644 index 0000000..26b0188 --- /dev/null +++ b/structutils.py | |||
@@ -0,0 +1,108 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Utilities for Manipulating Byte Streams | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 05, 2010 | ||
8 | """ | ||
9 | |||
10 | import struct | ||
11 | |||
12 | |||
13 | INT_FORMAT = struct.Struct('>I') | ||
14 | |||
15 | |||
16 | def pack_string(string): | ||
17 | """ | ||
18 | Pack a string into a network style byte array. | ||
19 | """ | ||
20 | return pack_int(len(string)) + string | ||
21 | |||
22 | |||
23 | def pack_int(integer): | ||
24 | """ | ||
25 | Pack an integer into a byte array. | ||
26 | """ | ||
27 | return INT_FORMAT.pack(integer) | ||
28 | |||
29 | |||
30 | def pack_mp_int(mp_int): | ||
31 | """ | ||
32 | Pack a multiple-percision integer into a byte array. | ||
33 | """ | ||
34 | return pack_string(int_to_bytes(mp_int)) | ||
35 | |||
36 | |||
37 | def unpack_string(buf): | ||
38 | """ | ||
39 | Unpack a string from a byte array buffer returning | ||
40 | the string and the remainder of the buffer. | ||
41 | """ | ||
42 | length, = INT_FORMAT.unpack(buf[:4]) | ||
43 | string = buf[4:4+length] | ||
44 | remainder = buf[4+length:] | ||
45 | return string, remainder | ||
46 | |||
47 | |||
48 | def unpack_mp_int(buf): | ||
49 | """ | ||
50 | Unpack a multiple-percision integer from a byte array | ||
51 | buffer returning the string and the remainder of the | ||
52 | buffer. | ||
53 | """ | ||
54 | length, = INT_FORMAT.unpack(buf[:4]) | ||
55 | remainder = buf[4+length:] | ||
56 | |||
57 | return bytes_to_int(buf[4:4+length]), remainder | ||
58 | |||
59 | |||
60 | def unpack_int(buf): | ||
61 | """ | ||
62 | Unpack an integer from a byte array buffer returning the | ||
63 | string and the remainder of the buffer. | ||
64 | """ | ||
65 | integer, = INT_FORMAT.unpack(buf[:4]) | ||
66 | remainder = buf[4:] | ||
67 | return integer, remainder | ||
68 | |||
69 | |||
70 | def get_packed_mp_ints(buf, count=1): | ||
71 | """ | ||
72 | Get a number of multiple-percision integers from a byte | ||
73 | array buffer but leaves them as network style mpints. | ||
74 | """ | ||
75 | ints = [] | ||
76 | for _ in range(count): | ||
77 | length, = INT_FORMAT.unpack(buf[:4]) | ||
78 | ints.append(buf[:4+length] | ||
79 | buf = buf[4+length:] | ||
80 | |||
81 | return ints | ||
82 | |||
83 | |||
84 | def int_to_bytes(integer): | ||
85 | """ | ||
86 | Convert an integer or a long integer to an array of | ||
87 | bytes. | ||
88 | """ | ||
89 | bytes = [] | ||
90 | |||
91 | while integer > 0: | ||
92 | integer, chunk = divmod(integer, 256) | ||
93 | bytes.insert(0, chr(chunk)) | ||
94 | |||
95 | return ''.join(bytes) | ||
96 | |||
97 | |||
98 | def bytes_to_int(buf): | ||
99 | """ | ||
100 | Convert an array of bytes into an integer or long | ||
101 | integer. | ||
102 | """ | ||
103 | integer = 0 | ||
104 | for byte in buf: | ||
105 | integer <<= 8 | ||
106 | integer += ord(byte) | ||
107 | |||
108 | return integer | ||