Artifact [973f5211fa]

Artifact 973f5211faa50d080b4fc668dd7bb950aa61b20f:


#! /usr/bin/env tclsh

set dir [file dirname [info script]]

if {[info exists ::env(SSH_AGENT_LIB_PATH)]} {
	lappend auto_path {*}$::env(SSH_AGENT_LIB_PATH)
}

if {![info exists ::env(SSH_AUTH_SOCK)]} {
	error "Must set SSH_AUTH_SOCK"
}

if {[info exists ::env(SSH_AGENT_PKCS11_MODULE)]} {
	set ::pkcs11ModuleFilename $::env(SSH_AGENT_PKCS11_MODULE)
} else {
	error "Must set SSH_AGENT_PKCS11_MODULE"
}

package require duktape 0.7
package require tuapi
package require pki 0.6
package require pki::pkcs11 0.9.9

# Files
set ::files(chrome-emu.js) {
@@CHROME_EMU_JS@@
}

set ::files(ssh-agent-noasync.js) {
@@SSH_AGENT_NOASYNC_JS@@
}

## HACK: Fix up older versions of "pki" to include the raw certificate
##       this is needed
apply {{} {
	set procToUpdate ::pki::x509::parse_cert
	if {![string match "*set ret(raw)*" [info body $procToUpdate]]} {
		set body [info body $procToUpdate]
		set body [string map {
			"::asn::asnGetSequence cert_seq wholething"
			"set ret(raw) $cert_seq; binary scan $ret(raw) H* ret(raw); ::asn::asnGetSequence cert_seq wholething"
		} $body]
		proc $procToUpdate [info args $procToUpdate] $body
	}
}}

proc pkcs11ModuleLogin {slot} {
	set pin [exec cackey-askpass]
	if {$pin eq ""} {
		error "No PIN provided"
	}

	::pki::pkcs11::login $::pkcs11ModuleHandle $slot $pin
}

proc pkcs11ModuleHandle {} {
	if {![info exists ::pkcs11ModuleHandle]} {
		set ::pkcs11ModuleHandle [::pki::pkcs11::loadmodule $::pkcs11ModuleFilename]
	}
	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]]

		# Pad to even size for hex width
		if {[string length $e] % 2 != 0} {
			set e "0$e"
		}
		if {[string length $n] % 2 != 0} {
			set n "0$n"
		}

		# Add a leading zero if the value is high enough
		if {"0x[string range $n 0 1]" > 0x7f} {
			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-512" {
				set data_b64 [binary encode base64 $data]
				set checksum [exec base64 -d << $data_b64 | sha512sum]
				set checksum [lindex [split $checksum] 0]
				return [binary decode hex $checksum]
			}
			"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 readFile {fileName} {
	if {[info exists ::files($fileName)]} {
		set data $::files($fileName)
		if {[string range [string trim $data] 0 1] ne "@@"} {
			return $data
		}
	}

	set fileName [file join $::dir $fileName]
	if {![info exists ::readFile($fileName)]} {
		catch {
			set fd [open $fileName]
			set ::readFile($fileName) [read $fd]
		}
		catch {
			close $fd
		}
	}

	return $::readFile($fileName)
}

proc initSSHAgent {} {
	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 [readFile chrome-emu.js]
	addRSAToJS $jsHandle
	::duktape::eval $jsHandle [readFile ssh-agent-noasync.js]
	::duktape::eval $jsHandle {cackeySSHAgentFeatures.enabled = true;}
	::duktape::eval $jsHandle {cackeySSHAgentFeatures.includeCerts = true;}
	::duktape::eval $jsHandle {cackeySSHAgentFeatures.legacy = false;}
	::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 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"
		}

		set slotId [dict get $certInfo pkcs11_slotid]
		try {
			set data [::pki::sign $message $certInfo raw]
		} on error {} {
			pkcs11ModuleLogin $slotId

			set data [::pki::sign $message $certInfo raw]
		}

		return $data
	}

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

	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]

		# Skip missing tokens
		if {"TOKEN_PRESENT" ni $slotFlags} {
			continue
		}

		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 ""} {
		set certs [list]
	}

	if {[llength $certs] == 0} {
		pkcs11ModuleUnload $handle
	}

	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} {
	if {[catch {
		if {![info exists ::jsHandle]} {
			set ::jsHandle [initSSHAgent]
		}
		set jsHandle $::jsHandle

		::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
	}
}

::tuapi::syscall::socket_unix -server incomingConnection $::env(SSH_AUTH_SOCK)

vwait forever