diff options
author | Mike Crute <mcrute@gmail.com> | 2010-05-05 22:15:17 -0400 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2010-05-05 22:15:17 -0400 |
commit | 5b8234f27f00ad135d24063dd430de35db43e42f (patch) | |
tree | cb00b67a439d24036fcfbd4a263343e7d5c2b394 | |
parent | b897f03e9f1690c05d01e6c697d2d6917e747c48 (diff) | |
download | hg_sshsign-5b8234f27f00ad135d24063dd430de35db43e42f.tar.bz2 hg_sshsign-5b8234f27f00ad135d24063dd430de35db43e42f.tar.xz hg_sshsign-5b8234f27f00ad135d24063dd430de35db43e42f.zip |
First pass at the plugin. Friggin huge commit.
-rw-r--r-- | keymanifest.py | 4 | ||||
-rw-r--r-- | keys.py | 91 | ||||
-rw-r--r-- | ssh.py | 143 | ||||
-rw-r--r-- | sshagent.py | 4 |
4 files changed, 195 insertions, 47 deletions
diff --git a/keymanifest.py b/keymanifest.py index df41f1e..25fa423 100644 --- a/keymanifest.py +++ b/keymanifest.py | |||
@@ -7,7 +7,7 @@ Key Manifest | |||
7 | @date: May 05, 2010 | 7 | @date: May 05, 2010 |
8 | """ | 8 | """ |
9 | 9 | ||
10 | from keys import load_public_key | 10 | from keys import PublicKey |
11 | 11 | ||
12 | 12 | ||
13 | class KeyManifest(dict): | 13 | class KeyManifest(dict): |
@@ -33,4 +33,4 @@ class KeyManifest(dict): | |||
33 | return inst | 33 | return inst |
34 | 34 | ||
35 | def __getitem__(self, key): | 35 | def __getitem__(self, key): |
36 | return load_public_key(dict.__getitem__(self, key)) | 36 | return PublicKey.from_string(dict.__getitem__(self, key)) |
@@ -7,43 +7,82 @@ Key Loader Functions | |||
7 | @date: May 05, 2010 | 7 | @date: May 05, 2010 |
8 | """ | 8 | """ |
9 | 9 | ||
10 | import os | ||
10 | from M2Crypto import RSA, DSA | 11 | from M2Crypto import RSA, DSA |
12 | from M2Crypto.EVP import MessageDigest | ||
13 | from M2Crypto.RSA import RSAError | ||
14 | from M2Crypto.DSA import DSAError | ||
11 | from structutils import unpack_string, get_packed_mp_ints, int_to_bytes | 15 | from structutils import unpack_string, get_packed_mp_ints, int_to_bytes |
12 | 16 | ||
13 | 17 | ||
14 | def load_public_key(key): | 18 | class PublicKey(object): |
15 | """ | 19 | |
16 | Loads an RFC 4716 formatted public key. | 20 | def __init__(self, hashed=None, instance=None, key_type=None): |
17 | """ | 21 | self.instance = instance |
18 | if key.startswith('ssh-'): | 22 | self.hashed = hashed |
19 | blob = key.split()[1].decode('base64') | 23 | self.key_type = key_type |
20 | else: | 24 | |
21 | blob = key.decode('base64') | 25 | @property |
26 | def blob(self): | ||
27 | return self.hashed.decode('base64') | ||
28 | |||
29 | def verify(self, data, signature): | ||
30 | try: | ||
31 | return bool(self.instance.verify(data, signature)) | ||
32 | except (RSAError, DSAError): | ||
33 | return False | ||
34 | |||
35 | def sign(self, data): | ||
36 | return self.instance.sign(data) | ||
37 | |||
38 | @property | ||
39 | def from_string(cls, key): | ||
40 | """ | ||
41 | Loads an RFC 4716 formatted public key. | ||
42 | """ | ||
43 | pubkey = cls() | ||
22 | 44 | ||
23 | ktype, remainder = unpack_string(blob) | 45 | if key.startswith('ssh-'): |
46 | pubkey.hashed = key.split()[1] | ||
47 | else: | ||
48 | pubkey.hashed = key | ||
24 | 49 | ||
25 | if ktype == 'ssh-rsa': | 50 | pubkey.key_type, remainder = unpack_string(pubkey.blob) |
26 | e, n = get_packed_mp_ints(remainder, 2) | ||
27 | return RSA.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 | 51 | ||
32 | raise ValueError("Invalid key") | 52 | if pubkey.key_type == 'ssh-rsa': |
53 | e, n = get_packed_mp_ints(remainder, 2) | ||
54 | pubkey.instance = RSA.new_pub_key((e, n)) | ||
55 | elif pubkey.key_type == 'ssh-dss': | ||
56 | p, q, g, y = get_packed_mp_ints(remainder, 4) | ||
57 | pubkey.instance = DSA.set_params(p, q, g) | ||
58 | |||
59 | return pubkey | ||
60 | |||
61 | @property | ||
62 | def from_file(cls, filename): | ||
63 | fp = open(filename) | ||
64 | try: | ||
65 | return cls.from_string(fp.read()) | ||
66 | finally: | ||
67 | fp.close() | ||
33 | 68 | ||
34 | 69 | ||
35 | def load_private_key(filename): | 70 | def load_private_key(filename): |
36 | first_line = open(filename).readline() | 71 | fp = open(filename) |
72 | try: | ||
73 | first_line = fp.readline() | ||
74 | finally: | ||
75 | fp.close() | ||
76 | |||
37 | type = DSA if 'DSA' in first_line else RSA | 77 | type = DSA if 'DSA' in first_line else RSA |
38 | return type.load_key(filename) | 78 | return type.load_key(filename) |
39 | 79 | ||
40 | 80 | ||
41 | def sign(what, key): | 81 | def sign_like_agent(data, key): |
42 | pk = load_private_key(key) | 82 | """ |
43 | val = pk.sign(what, None)[0] | 83 | Emulates the signing behavior of an ssh key agent. |
44 | return int_to_bytes(val) | 84 | """ |
45 | 85 | digest = MessageDigest('sha1') | |
46 | 86 | digest.update(data) | |
47 | def verify(what, signature, key): | 87 | my_data = digest.final() |
48 | signature = int_to_bytes(signature) | 88 | return key.sign(data) |
49 | return bool(key.verify(what, signature)) | ||
@@ -14,30 +14,137 @@ Ponder this, bitches: | |||
14 | http://svn.osafoundation.org/m2crypto/trunk/SWIG/_rsa.i | 14 | http://svn.osafoundation.org/m2crypto/trunk/SWIG/_rsa.i |
15 | """ | 15 | """ |
16 | 16 | ||
17 | from M2Crypto.RSA import RSAError | ||
18 | 17 | ||
18 | import keys | ||
19 | from keymanifest import KeyManifest | ||
19 | from structutils import bytes_to_int | 20 | from structutils import bytes_to_int |
20 | from sshagent import SSHAgent | 21 | from sshagent import SSHAgent |
21 | from keys import load_private_key, load_public_key | ||
22 | 22 | ||
23 | test_data = "Hello world!" | 23 | import os, tempfile, binascii |
24 | public_key = "/Users/mcrute/.ssh/id_rsa.ag.pub" | 24 | from mercurial import util, commands, match |
25 | private_key = "/Users/mcrute/.ssh/id_rsa.ag" | 25 | from mercurial import node as hgnode |
26 | from mercurial.i18n import _ | ||
26 | 27 | ||
27 | key = open(public_key).read() | ||
28 | key = key.split()[1].decode('base64') | ||
29 | 28 | ||
30 | agent = SSHAgent() | 29 | def absolute_path(path): |
31 | signature = agent.sign(test_data, key) | 30 | path = os.path.expandvars(path) |
32 | print bytes_to_int(signature) | 31 | return os.path.expanduser(path) |
33 | 32 | ||
34 | 33 | ||
35 | try: | 34 | class SSHAuthority(object): |
36 | pub_key = load_public_key(open(public_key).read()) | 35 | |
37 | pub_key.verify(test_data, signature) | 36 | @classmethod |
38 | print "Signature matches" | 37 | def from_ui(cls, ui): |
39 | except RSAError: | 38 | public_key = absolute_path(ui.config("sshsign", "public_key")) |
40 | print "Signature doesn't match" | 39 | private_key = absolute_path(ui.config("sshsign", "private_key")) |
40 | manifest_file = absolute_path(ui.config("sshsign", "manifest_file")) | ||
41 | |||
42 | manifest = KeyManifest.from_file(manifest_file) | ||
43 | public_key = keys.PublicKey.from_file(public_key) | ||
44 | |||
45 | def __init__(self, public_key, key_manifest=None, private_key=None): | ||
46 | self.public_key = public_key | ||
47 | self.key_manifest = key_manifest | ||
48 | self.private_key = private_key | ||
49 | |||
50 | def verify(self, data, signature, whom): | ||
51 | key = self.key_manifest[whom] # XXX: More elegant error handling. | ||
52 | return key.verify(data, signature) | ||
53 | |||
54 | def sign(self, data): | ||
55 | return self.private_key.sign(data) | ||
56 | |||
57 | |||
58 | |||
59 | def node2txt(repo, node, ver): | ||
60 | """map a manifest into some text""" | ||
61 | if ver != "0": | ||
62 | raise util.Abort(_("unknown signature version")) | ||
63 | |||
64 | return "%s\n" % hgnode.hex(node) | ||
65 | |||
66 | |||
67 | def sign(ui, repo, *revs, **opts): | ||
68 | """add a signature for the current or given revision | ||
69 | |||
70 | If no revision is given, the parent of the working directory is used, | ||
71 | or tip if no revision is checked out. | ||
72 | |||
73 | See 'hg help dates' for a list of formats valid for -d/--date. | ||
74 | """ | ||
75 | |||
76 | mygpg = SSHAuthority.from_ui(ui) | ||
77 | sigver = "0" | ||
78 | sigmessage = "" | ||
79 | |||
80 | date = opts.get('date') | ||
81 | if date: | ||
82 | opts['date'] = util.parsedate(date) | ||
83 | |||
84 | if revs: | ||
85 | nodes = [repo.lookup(n) for n in revs] | ||
86 | else: | ||
87 | nodes = [node for node in repo.dirstate.parents() | ||
88 | if node != hgnode.nullid] | ||
89 | if len(nodes) > 1: | ||
90 | raise util.Abort(_('uncommitted merge - please provide a ' | ||
91 | 'specific revision')) | ||
92 | if not nodes: | ||
93 | nodes = [repo.changelog.tip()] | ||
94 | |||
95 | for n in nodes: | ||
96 | hexnode = hgnode.hex(n) | ||
97 | ui.write(_("Signing %d:%s\n") % (repo.changelog.rev(n), | ||
98 | hgnode.short(n))) | ||
99 | # build data | ||
100 | data = node2txt(repo, n, sigver) | ||
101 | sig = mygpg.sign(data) | ||
102 | if not sig: | ||
103 | raise util.Abort(_("Error while signing")) | ||
104 | sig = binascii.b2a_base64(sig) | ||
105 | sig = sig.replace("\n", "") | ||
106 | sigmessage += "%s %s %s\n" % (hexnode, sigver, sig) | ||
107 | |||
108 | # write it | ||
109 | if opts['local']: | ||
110 | repo.opener("localsigs", "ab").write(sigmessage) | ||
111 | return | ||
112 | |||
113 | msigs = match.exact(repo.root, '', ['.hgsigs']) | ||
114 | s = repo.status(match=msigs, unknown=True, ignored=True)[:6] | ||
115 | if util.any(s) and not opts["force"]: | ||
116 | raise util.Abort(_("working copy of .hgsigs is changed " | ||
117 | "(please commit .hgsigs manually " | ||
118 | "or use --force)")) | ||
119 | |||
120 | repo.wfile(".hgsigs", "ab").write(sigmessage) | ||
121 | |||
122 | if '.hgsigs' not in repo.dirstate: | ||
123 | repo.add([".hgsigs"]) | ||
124 | |||
125 | if opts["no_commit"]: | ||
126 | return | ||
127 | |||
128 | message = opts['message'] | ||
129 | if not message: | ||
130 | # we don't translate commit messages | ||
131 | message = "\n".join(["Added signature for changeset %s" | ||
132 | % hgnode.short(n) | ||
133 | for n in nodes]) | ||
134 | try: | ||
135 | repo.commit(message, opts['user'], opts['date'], match=msigs) | ||
136 | except ValueError, inst: | ||
137 | raise util.Abort(str(inst)) | ||
138 | |||
139 | |||
140 | cmdtable = { | ||
141 | "sign": | ||
142 | (sign, | ||
143 | [('l', 'local', None, _('make the signature local')), | ||
144 | ('f', 'force', None, _('sign even if the sigfile is modified')), | ||
145 | ('', 'no-commit', None, _('do not commit the sigfile after signing')), | ||
146 | ('m', 'message', '', _('commit message')), | ||
147 | ] + commands.commitopts2, | ||
148 | _('hg sign [OPTION]... [REVISION]...')), | ||
149 | } | ||
41 | 150 | ||
42 | priv_key = load_private_key(private_key) | ||
43 | print bytes_to_int(priv_key.sign(test_data)) | ||
diff --git a/sshagent.py b/sshagent.py index 8c310f4..6803b89 100644 --- a/sshagent.py +++ b/sshagent.py | |||
@@ -20,11 +20,13 @@ class SSHAgent(object): | |||
20 | SSH Agent communication protocol for signing only. | 20 | SSH Agent communication protocol for signing only. |
21 | """ | 21 | """ |
22 | 22 | ||
23 | AGENT_SOCK_NAME = 'SSH_AUTH_SOCK' | ||
24 | |||
23 | SSH2_AGENT_SIGN_RESPONSE = 14 | 25 | SSH2_AGENT_SIGN_RESPONSE = 14 |
24 | SSH2_AGENTC_SIGN_REQUEST = 13 | 26 | SSH2_AGENTC_SIGN_REQUEST = 13 |
25 | 27 | ||
26 | def __init__(self, socket_path=None): | 28 | def __init__(self, socket_path=None): |
27 | default_path = os.environ.get('SSH_AUTH_SOCK') | 29 | default_path = os.environ.get(SSHAgent.AGENT_SOCK_NAME) |
28 | socket_path = default_path if not socket_path else socket_path | 30 | socket_path = default_path if not socket_path else socket_path |
29 | 31 | ||
30 | if not socket_path: | 32 | if not socket_path: |