Index: build/tcl/ssh-agent.tcl ================================================================== --- build/tcl/ssh-agent.tcl +++ build/tcl/ssh-agent.tcl @@ -1,33 +1,359 @@ -#! /home/rkeene/tmp/cackey/build/tcl/tclkit +#! /usr/bin/env tclsh +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 {} { - if {[info exists ::jsHandle]} { - return - } - - set chromeEmuJS [read [open chrome-emu.js]] - set sshAgentJS [read [open ssh-agent-noasync.js]] - - set ::jsHandle [::duktape::init] - - ::duktape::eval $::jsHandle $chromeEmuJS - ::duktape::eval $::jsHandle $sshAgentJS - - puts [::duktape::eval $::jsHandle { - chrome.runtime.connectCallbacks[0]({ - sender: { - id: "pnhechapfaindjhompbnflcldabbghjo" - }, - onMessage: { - addListener: function() { - /* XXX:TODO */ - } - } - }) - }] -} - -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 + } + } + + 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 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] + 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} { + 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 + } +} + +::tuapi::syscall::socket_unix -server incomingConnection "./agent" + +vwait forever