Check-in [7ef094be65]
Overview
Comment:Tcl-based interface to JS SSH Agent
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 7ef094be65e2edf642f6e5b57e7ad7a4edd5710b
User & Date: rkeene on 2019-06-12 19:40:23
Other Links: manifest | tags
Context
2019-06-12
22:41
More testing of the Tcl implementation of the SSH agent check-in: 24e37c4dab user: rkeene tags: trunk
19:40
Tcl-based interface to JS SSH Agent check-in: 7ef094be65 user: rkeene tags: trunk
19:39
More work Duktape-based chrome-alike environment check-in: d73ab988ad user: rkeene tags: trunk
Changes

Modified build/tcl/ssh-agent.tcl from [0cbe74a449] to [757d0fe647].


1


2




3
4






































































































5
6










7



















































































































































8
9
10

11
12
13
14






15







16
17
18
19
20
21
22
23
24
25




















































26
27
28







29
30





31
32
33





1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275




276
277
278
279
280
281
282
283
284
285
286
287
288
289










290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341



342
343
344
345
346
347
348


349
350
351
352
353
354


355
356
357
358
359
+
-
+
+

+
+
+
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



+
-
-
-
-
+
+
+
+
+
+

+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+

-
-
+
+
+
+
+
#! /usr/bin/env tclsh
#! /home/rkeene/tmp/cackey/build/tcl/tclkit

lappend auto_path /home/rkeene/devel/tcl-duktape/build/work /home/rkeene/devel/tuapi /home/rkeene/devel/tclpkcs11-fossil/build/work {*}[glob -nocomplain -directory /opt/appfs/rkeene.org/tcllib/platform/latest/lib/ tcllib*]

package provide pki 0.10
catch {
	source /home/rkeene/devel/tcllib-pki/pki.tcl
}
package require duktape
package require tuapi
package require pki::pkcs11

proc pkcs11ModuleHandle {} {
	if {![info exists ::pkcs11ModuleHandle]} {
		set ::pkcs11ModuleHandle [::pki::pkcs11::loadmodule /home/rkeene/tmp/cackey/build/tcl/softokn3-pkcs11.so]
	}
	return $::pkcs11ModuleHandle
}

proc pkcs11ModuleUnload {handle} {
	if {[info exists ::pkcs11ModuleHandle] && $handle eq $::pkcs11ModuleHandle} {
		unset ::pkcs11ModuleHandle
	}
	::pki::pkcs11::unloadmodule $handle
}

proc addRSAToJS {jsHandle} {
	::duktape::tcl-function $jsHandle __parseCert json {cert} {
		set cert [binary decode hex $cert]
		if {[catch {
			set cert [::pki::x509::parse_cert $cert]
		}]} {
			return ""
		}

		set e [format %llx [dict get $cert e]]
		set n [format %llx [dict get $cert n]]
		if {[string length $e] % 2 != 0} {
			set e "0$e"
		}
		if {[string length $n] % 2 != 0} {
			set n "0$n"
		}
		if {[string length $n] % 4 == 0} {
			set n "00$n"
		}

		set retval "\{
			\"publicKey\": \{
				\"type\":\"[string toupper [dict get $cert type]]\",
				\"e\":\"$e\",
				\"n\":\"$n\"
			\},
			\"subject\": \"[dict get $cert subject]\",
			\"issuer\": \"[dict get $cert issuer]\",
			\"serial\": \"[dict get $cert serial_number]\"
		\}"

		return $retval
	}

	::duktape::tcl-function $jsHandle __crypto_subtle_digest bytearray {hash data} {
		switch -exact -- $hash {
			"SHA-256" {
				package require sha256
				return [::sha2::sha256 -- $data]
			}
			"SHA-1" {
				package require sha1
				return [::sha1::sha1 -- $data]
			}
			default {
				error "Hash not supported: $hash"
			}
		}
	}

	::duktape::eval $jsHandle {
		crypto.subtle.digest.internal = __crypto_subtle_digest;
		delete __crypto_subtle_digest;
	}

	::duktape::eval $jsHandle {
		function X509() {
			this.hex = "";
			this.readCertHex = function(string) {
				this.hex = string;
			};
			this.computeCertData = function() {
				if (this.certData) {
					return;
				}
				this.certData = X509.parseCert(this.hex);
				this.certData.publicKey.n = Duktape.dec('hex', this.certData.publicKey.n);
				this.certData.publicKey.e = Duktape.dec('hex', this.certData.publicKey.e);
			}
			this.getPublicKey = function() {
				this.computeCertData();
				return(this.certData.publicKey);
			};
			this.getSubjectString = function() {
				this.computeCertData();
				return(this.certData.subject);
			};
			this.getExtSubjectAltName2 = function() {
				return([]);
			}
		}
		X509.parseCert = __parseCert;
		delete __parseCert;
	}
}

proc initSSHAgent {} {
	foreach file {chrome-emu.js ssh-agent-noasync.js} {
		unset -nocomplain fd
		catch {
			set fd [open $file]
			set js($file) [read $fd]
		}
		catch {
			close $fd
		}
	}
	if {[info exists ::jsHandle]} {

	set jsHandle [::duktape::init -safe true]

	::duktape::tcl-function $jsHandle __puts {args} {
		if {[llength $args] ni {1 2}} {
			return -code error "wrong # args: puts ?{stderr|stdout}? message"
		}
		if {[llength $args] == 2} {
			set chan [lindex $args 0]
			if {$chan ni {stdout stderr}} {
				return -code error "Only stderr and stdout allowed"
			}
		}
		puts {*}$args
	}

	::duktape::eval $jsHandle {
		runtime = {};
		runtime.puts = __puts;
		runtime.stderr = "stderr";
		delete __puts;
	}

	::duktape::eval $jsHandle {var goog = {DEBUG: false};}
	::duktape::eval $jsHandle $js(chrome-emu.js)
	addRSAToJS $jsHandle
	::duktape::eval $jsHandle $js(ssh-agent-noasync.js)
	::duktape::eval $jsHandle {cackeySSHAgentFeatures.enabled = true;}
	::duktape::eval $jsHandle {cackeySSHAgentFeatures.includeCerts = true;}
	::duktape::eval $jsHandle {
		function connection(callback) {
			this.sender = {
				id: "pnhechapfaindjhompbnflcldabbghjo"
			};
			this.onMessage = {
				listeners: [],
				addListener: function(callback) {
					this.listeners.push(callback);
				}
			};
			this.postMessage = function(message) {
				return(callback(this, message));
			};
			this.send = function(message) {
				this.onMessage.listeners.forEach(function(listener) {
					listener(message);
				});
			};
		}

		function handleDataFromAgent(socket, data) {
			if (!data || !data.type || !data.data) {
				return;
			}

			if (data.type != "auth-agent@openssh.com") {
				return;
			}

			writeFramed(socket.handle, data.data);
		}

		function handleDataFromSocket(socket, data) {
			socket.send({
				type: "auth-agent@openssh.com",
				data: Array.from(data)
			});
		}

		function writeFramed(sock, data) {
			var buffer;
			var idx;

			buffer = new Buffer(data.length);
			for (idx = 0; idx < data.length; idx++) {
				buffer[idx] = data[idx];
			}
			return(writeFramedBuffer(sock, buffer));
		}

		function cackeyListCertificates() {
			var certs;
			var certObjs;

			certObjs = [];
			certs = cackeyListCertificatesBare();
			certs.forEach(function(cert) {
				certObjs.push({
					certificate: new Uint8Array(cert),
					supportedHashes: ['SHA1', 'SHA256', 'SHA512', 'MD5_SHA1']
				});
			});

			return(certObjs);
		}

		function cackeySignMessage(request) {
			var retval;
			var digest, digestHeader;

			/*
			 * XXX:TODO: Pull this out of cackey.js into a common.js
			 */
			switch (request.hash) {
				case "SHA1":
					digestHeader = new Uint8Array([0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14]);
					break;
				case "SHA256":
					digestHeader = new Uint8Array([0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20]);
					break;
				case "SHA512":
					digestHeader = new Uint8Array([0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40]);
					break;
				case "MD5_SHA1":
				case "RAW":
					digestHeader = new Uint8Array();
					break;
				default:
					console.error("[cackey] Asked to sign a message with a hash we do not support: " + request.hash);
					return(null);
			}

			digest = Array.from(digestHeader);
			digest = digest.concat(Array.from(request.digest));
			digest = new Buffer(digest);

			retval = cackeySignBare(request.certificate, digest);

			return(retval);
		}
	}

	::duktape::tcl-function $jsHandle writeFramedBuffer {sock message} {
		set dataLen [string length $message]
		set dataLen [binary format I $dataLen]
		puts -nonewline $sock "${dataLen}${message}"
		flush $sock

		return ""
	}

	::duktape::tcl-function $jsHandle readFramed bytearray {sock} {
		catch {
			set dataLen [read $sock 4]
		}
		if {![info exists dataLen] || [string length $dataLen] != 4} {
			close $sock
		return
	}

		binary scan $dataLen I dataLen
	set chromeEmuJS [read [open chrome-emu.js]]
	set sshAgentJS [read [open ssh-agent-noasync.js]]

	set ::jsHandle [::duktape::init]

		set data [read $sock $dataLen]

		return $data
	}


	::duktape::tcl-function $jsHandle cackeySignBare bytearray {cert message} {
		set handle [pkcs11ModuleHandle]
		set certInfo [listCerts $handle $cert]
		if {![dict exists $certInfo pkcs11_slotid]} {
			pkcs11ModuleUnload $handle
			return -code error "Unable to find certificate to sign with"
		}
	::duktape::eval $::jsHandle $chromeEmuJS
	::duktape::eval $::jsHandle $sshAgentJS

	puts [::duktape::eval $::jsHandle {
		chrome.runtime.connectCallbacks[0]({
			sender: {
				id: "pnhechapfaindjhompbnflcldabbghjo"
			},
			onMessage: {
				addListener: function() {

		set slotId [dict get $certInfo pkcs11_slotid]
		set data [::pki::sign $message $certInfo raw]

		return $data
	}

	::duktape::tcl-function $jsHandle cackeyListCertificatesBare {arraylist bytearray} {} {
		set handle [pkcs11ModuleHandle]
		return [listCerts $handle]
	}

	return $jsHandle
}

proc listCerts {handle {match ""}} {
	set certs [list]

	set slots [::pki::pkcs11::listslots $handle]
	foreach slotInfo $slots {
		set slotId [lindex $slotInfo 0]
		set slotLabel [lindex $slotInfo 1]
		set slotFlags [lindex $slotInfo 2]

		set slotCerts [::pki::pkcs11::listcerts $handle $slotId]
		foreach keyList $slotCerts {
			set cert [dict get $keyList raw]
			set cert [binary decode hex $cert]
			if {$match eq $cert} {
				return $keyList
			}
			lappend certs $cert
		}
	}

	if {$match ne ""} {
		return [list]
	}

	return $certs
}

proc handleData {sock jsHandle} {
	if {[catch {
		::duktape::eval $jsHandle {handleDataFromSocket(socket, readFramed(socket.handle));}
	}]} {
		puts stderr "ERROR: $::errorInfo"
		close $sock
	}
}

proc incomingConnection {sock args} {
					/* XXX:TODO */
				}
			}
	if {[catch {
		set jsHandle [initSSHAgent]

		::duktape::eval $jsHandle {var socket = new connection(handleDataFromAgent);}
		::duktape::eval $jsHandle "socket.handle = \"$sock\";"
		::duktape::eval $jsHandle {chrome.runtime.externalConnect(socket);}

		})
	}]
		fconfigure $sock -translation binary -encoding binary -blocking true
		fileevent $sock readable [list handleData $sock $jsHandle]
	}]} {
		puts stderr "ERROR: $::errorInfo"
		close $sock
}

initSSHAgent
}

::tuapi::syscall::socket_unix -server incomingConnection "./agent"

vwait forever