New tool/pageant_proxy_to_gpg.py
[gnuk/gnuk.git] / tool / pageant_proxy_to_gpg.py
1 import os, sys, re, hashlib, binascii
2 from struct import *
3 from gpg_agent import gpg_agent
4 from sexp import sexp
5
6 # Assume it's only OPENPGP.3 key and it's 2048-bit
7
8 def debug(string):
9     print "DEBUG: %s" % string
10     sys.stdout.flush()
11
12 def get_keygrip_list(keyinfo_result):
13     kl_str = keyinfo_result[0:-1] # Chop last newline
14     kl = kl_str.split('\n')
15     # filter by "OPENPGP.3", and only keygrip
16     return [kg.split(' ')[0] for kg in kl if re.search("OPENPGP\\.3", kg)]
17
18 # Connect GPG-Agent, and get list of KEYGRIPs.
19 g = gpg_agent()
20 g.read_line()                   # Greeting message
21
22 g.send_command('KEYINFO --list --data\n')
23 keyinfo_result = g.get_response()
24 keygrip_list = get_keygrip_list(keyinfo_result)
25
26 debug(keygrip_list)
27
28 keylist = []
29 # For each KEYGRIP, get its PUBLIC-KEY.
30 for kg in keygrip_list:
31     g.send_command('READKEY %s\n' % kg)
32     key = sexp(g.get_response())
33     # [ "public-key" [ "rsa" [ "n" MODULUS ] [ "e" EXPONENT] ] ]
34     n = key[1][1][1]
35     e = key[1][2][1]
36     debug(binascii.hexlify(n))
37     debug(binascii.hexlify(e))
38     keylist.append([n, e, kg])
39
40 # FIXME: should handle all keys, not only a single key
41 # FIXME: should support different key size
42 n = keylist[0][0]
43 e = keylist[0][1]
44 keygrip = keylist[0][2]
45
46 ssh_rsa_public_blob = "\x00\x00\x00\x07ssh-rsa" + \
47     "\x00\x00\x00\x03" + e + "\x00\x00\x01\x01" + n
48
49 ssh_key_comment = "key_on_gpg"  # XXX: get login from card for comment?
50
51 import win32con, win32api, win32gui, ctypes, ctypes.wintypes
52
53
54 # For WM_COPYDATA structure
55 class COPYDATA(ctypes.Structure):
56     _fields_ = [ ('dwData', ctypes.wintypes.LPARAM),
57                  ('cbData', ctypes.wintypes.DWORD),
58                  ('lpData', ctypes.c_void_p) ]
59
60 P_COPYDATA = ctypes.POINTER(COPYDATA)
61
62 class SSH_MSG_HEAD(ctypes.BigEndianStructure):
63     _pack_ = 1
64     _fields_ = [ ('msg_len', ctypes.c_uint32),
65                  ('msg_type', ctypes.c_byte) ]
66
67 P_SSH_MSG_HEAD = ctypes.POINTER(SSH_MSG_HEAD)
68
69 class SSH_MSG_ID_ANSWER_HEAD(ctypes.BigEndianStructure):
70     _pack_ = 1
71     _fields_ = [ ('msg_len', ctypes.c_uint32),
72                  ('msg_type', ctypes.c_byte),
73                  ('keys', ctypes.c_uint32)]
74
75 P_SSH_MSG_ID_ANSWER = ctypes.POINTER(SSH_MSG_ID_ANSWER_HEAD)
76
77 class SSH_MSG_SIGN_RESPONSE_HEAD(ctypes.BigEndianStructure):
78     _pack_ = 1
79     _fields_ = [ ('msg_len', ctypes.c_uint32),
80                  ('msg_type', ctypes.c_byte),
81                  ('sig_len', ctypes.c_uint32)]
82
83 P_SSH_MSG_SIGN_RESPONSE = ctypes.POINTER(SSH_MSG_SIGN_RESPONSE_HEAD)
84
85
86 FILE_MAP_ALL_ACCESS=0x000F001F
87
88 class windows_ipc_listener(object):
89     def __init__(self):
90         message_map = { win32con.WM_COPYDATA: self.OnCopyData }
91         wc = win32gui.WNDCLASS()
92         wc.lpfnWndProc = message_map
93         wc.lpszClassName = 'Pageant'
94         hinst = wc.hInstance = win32api.GetModuleHandle(None)
95         classAtom = win32gui.RegisterClass(wc)
96         self.hwnd = win32gui.CreateWindow (
97             classAtom,
98             "Pageant",
99             0,
100             0, 
101             0,
102             win32con.CW_USEDEFAULT, 
103             win32con.CW_USEDEFAULT,
104             0, 
105             0,
106             hinst, 
107             None
108         )
109         debug("created: window=%08x" % self.hwnd)
110
111     def OnCopyData(self, hwnd, msg, wparam, lparam):
112         debug("WM_COPYDATA message")
113         debug("  window=%08x" % hwnd)
114         debug("  msg   =%08x" % msg)
115         debug("  wparam=%08x" % wparam)
116         pCDS = ctypes.cast(lparam, P_COPYDATA)
117         debug("  dwData=%08x" % (pCDS.contents.dwData & 0xffffffff))
118         debug("  len=%d" % pCDS.contents.cbData)
119         mapname = ctypes.string_at(pCDS.contents.lpData)
120         debug("  mapname='%s'" % ctypes.string_at(pCDS.contents.lpData))
121         hMapObject = ctypes.windll.kernel32.OpenFileMappingA(FILE_MAP_ALL_ACCESS, 0, mapname)
122         if hMapObject == 0:
123             debug("error on OpenFileMapping")
124             return 0
125         pBuf = ctypes.windll.kernel32.MapViewOfFile(hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, 0)
126         if pBuf == 0:
127             ctypes.windll.kernel32.CloseHandle(hMapObject)
128             debug("error on MapViewOfFile")
129             return 0
130         pSshMsg = ctypes.cast(pBuf, P_SSH_MSG_HEAD)
131         debug("   ssh_msg_len: %d" % pSshMsg.contents.msg_len)
132         debug("   ssh_msg_type: %d" % pSshMsg.contents.msg_type)
133         if pSshMsg.contents.msg_type == 11: # SSH2_AGENT_REQUEST_IDENTITIES
134             blob_len = len(ssh_rsa_public_blob)
135             cmnt_len = len(ssh_key_comment)
136             pAns = ctypes.cast(pBuf, P_SSH_MSG_ID_ANSWER)
137             pAns.contents.msg_len = 1+4+4+blob_len+4+cmnt_len
138             pAns.contents.msg_type = 12 # SSH2_AGENT_IDENTITIES_ANSWER
139             pAns.contents.keys = 1
140             ctypes.memmove(pBuf+4+1+4, pack('>I', blob_len), 4)
141             ctypes.memmove(pBuf+4+1+4+4, ssh_rsa_public_blob, blob_len)
142             ctypes.memmove(pBuf+4+1+4+4+blob_len, pack('>I', cmnt_len), 4)
143             ctypes.memmove(pBuf+4+1+4+4+blob_len+4, ssh_key_comment, cmnt_len)
144
145             debug("answer is:")
146             debug("   ssh_msg_len: %d" % pSshMsg.contents.msg_len)
147             debug("   ssh_msg_type: %d" % pSshMsg.contents.msg_type)
148         elif pSshMsg.contents.msg_type == 13: # SSH2_AGENT_SIGN_REQUEST
149             req_blob_len = unpack(">I", ctypes.string_at(pBuf+5, 4))[0]
150             req_blob = ctypes.string_at(pBuf+5+4, req_blob_len)
151             req_data_len = unpack(">I", ctypes.string_at(pBuf+5+4+req_blob_len,4))[0]
152             req_data = ctypes.string_at(pBuf+5+4+req_blob_len+4,req_data_len)
153             debug("    blob_len=%d" % req_blob_len)
154             debug("    data_len=%d" % req_data_len)
155             hash = hashlib.sha1(req_data).hexdigest()
156             debug("    hash=%s" % hash)
157             g.send_command('SIGKEY %s\n' % keygrip)
158             g.send_command('SETHASH --hash=sha1 %s\n' % hash)
159             g.send_command('PKSIGN\n')
160             sig = sexp(g.get_response())
161             # [ "sig-val" [ "rsa" [ "s" "xxx" ] ] ]
162             sig = sig[1][1][1]
163             sig = "\x00\x00\x00\x07" + "ssh-rsa" + "\x00\x00\x01\x00" + sig # FIXME: should support different key size
164             siglen = len(sig)
165             debug("sig_len=%d" % siglen)
166             debug("sig=%s" % binascii.hexlify(sig))
167             pRes = ctypes.cast(pBuf, P_SSH_MSG_SIGN_RESPONSE)
168             pRes.contents.msg_len = 1+4+siglen
169             pRes.contents.msg_type = 14 # SSH2_AGENT_SIGN_RESPONSE
170             pRes.contents.sig_len = siglen
171             ctypes.memmove(pBuf+4+1+4, sig, siglen)
172             debug("answer is:")
173             debug("   ssh_msg_len: %d" % pSshMsg.contents.msg_len)
174             debug("   ssh_msg_type: %d" % pSshMsg.contents.msg_type)
175         else:
176             exit(0)
177         ctypes.windll.kernel32.UnmapViewOfFile(pBuf)
178         ctypes.windll.kernel32.CloseHandle(hMapObject)
179         debug("   ssh_msg: done")
180         return 1
181
182 l = windows_ipc_listener()
183 win32gui.PumpMessages()