#! /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 {} {
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