summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2010-05-05 22:15:17 -0400
committerMike Crute <mcrute@gmail.com>2010-05-05 22:15:17 -0400
commit5b8234f27f00ad135d24063dd430de35db43e42f (patch)
treecb00b67a439d24036fcfbd4a263343e7d5c2b394
parentb897f03e9f1690c05d01e6c697d2d6917e747c48 (diff)
downloadhg_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.py4
-rw-r--r--keys.py91
-rw-r--r--ssh.py143
-rw-r--r--sshagent.py4
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
10from keys import load_public_key 10from keys import PublicKey
11 11
12 12
13class KeyManifest(dict): 13class 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))
diff --git a/keys.py b/keys.py
index 8d3a5b2..33cb98e 100644
--- a/keys.py
+++ b/keys.py
@@ -7,43 +7,82 @@ Key Loader Functions
7@date: May 05, 2010 7@date: May 05, 2010
8""" 8"""
9 9
10import os
10from M2Crypto import RSA, DSA 11from M2Crypto import RSA, DSA
12from M2Crypto.EVP import MessageDigest
13from M2Crypto.RSA import RSAError
14from M2Crypto.DSA import DSAError
11from structutils import unpack_string, get_packed_mp_ints, int_to_bytes 15from structutils import unpack_string, get_packed_mp_ints, int_to_bytes
12 16
13 17
14def load_public_key(key): 18class 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
35def load_private_key(filename): 70def 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
41def sign(what, key): 81def 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)
47def 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))
diff --git a/ssh.py b/ssh.py
index b308f5e..0c60cb0 100644
--- a/ssh.py
+++ b/ssh.py
@@ -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
17from M2Crypto.RSA import RSAError
18 17
18import keys
19from keymanifest import KeyManifest
19from structutils import bytes_to_int 20from structutils import bytes_to_int
20from sshagent import SSHAgent 21from sshagent import SSHAgent
21from keys import load_private_key, load_public_key
22 22
23test_data = "Hello world!" 23import os, tempfile, binascii
24public_key = "/Users/mcrute/.ssh/id_rsa.ag.pub" 24from mercurial import util, commands, match
25private_key = "/Users/mcrute/.ssh/id_rsa.ag" 25from mercurial import node as hgnode
26from mercurial.i18n import _
26 27
27key = open(public_key).read()
28key = key.split()[1].decode('base64')
29 28
30agent = SSHAgent() 29def absolute_path(path):
31signature = agent.sign(test_data, key) 30 path = os.path.expandvars(path)
32print bytes_to_int(signature) 31 return os.path.expanduser(path)
33 32
34 33
35try: 34class 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):
39except 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
59def 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
67def 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
140cmdtable = {
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
42priv_key = load_private_key(private_key)
43print 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: