Recursive key fetching
diff --git a/js/tools/build/ndn-js.js b/js/tools/build/ndn-js.js
index 68efd9e..9dc4b33 100644
--- a/js/tools/build/ndn-js.js
+++ b/js/tools/build/ndn-js.js
@@ -9,14 +9,16 @@
 d.host);d.connectAndExpressInterest(a,b)},3E3);this.transport.expressInterest(this,c,new NDN.ConnectClosure(this,a,b,e))}};NDN.ConnectClosure=function(a,b,c,d){Closure.call(this);this.ndn=a;this.callerInterest=b;this.callerClosure=c;this.timerID=d};
 NDN.ConnectClosure.prototype.upcall=function(a){if(!(a==Closure.UPCALL_CONTENT||a==Closure.UPCALL_CONTENT_UNVERIFIED||a==Closure.UPCALL_INTEREST))return Closure.RESULT_ERR;clearTimeout(this.timerID);console.log(this.ndn.host+": Host is alive. Fetching callerInterest.");this.ndn.transport.expressInterest(this.ndn,this.callerInterest,this.callerClosure);return Closure.RESULT_OK};
 var WebSocketTransport=function(){this.ccndid=this.ws=null;this.maxBufferSize=1E4;this.buffer=new Uint8Array(this.maxBufferSize);this.bufferOffset=0;this.structureDecoder=new BinaryXMLStructureDecoder;this.defaultGetHostAndPort=NDN.makeShuffledGetHostAndPort(["A.ws.ndn.ucla.edu","B.ws.ndn.ucla.edu","C.ws.ndn.ucla.edu","D.ws.ndn.ucla.edu","E.ws.ndn.ucla.edu"],9696)};
-WebSocketTransport.prototype.connectWebSocket=function(a){null!=this.ws&&delete this.ws;this.ws=new WebSocket("ws://"+a.host+":"+a.port);0<LOG&&console.log("ws connection created.");this.ws.binaryType="arraybuffer";var b=this;this.ws.onmessage=function(c){var d=c.data;if(null==d||void 0==d||""==d)console.log("INVALID ANSWER");else if(d instanceof ArrayBuffer){var e=new Uint8Array(d);3<LOG&&console.log("BINARY RESPONSE IS "+DataUtils.toHex(e));try{if(e.length+b.bufferOffset>=b.buffer.byteLength){3<
-LOG&&console.log("NDN.ws.onmessage: buffer overflow. Accumulate received length: "+b.bufferOffset+". Current packet length: "+e.length+".");delete b.structureDecoder;delete b.buffer;b.structureDecoder=new BinaryXMLStructureDecoder;b.buffer=new Uint8Array(b.maxBufferSize);b.bufferOffset=0;return}b.buffer.set(e,b.bufferOffset);b.bufferOffset+=e.length;if(!b.structureDecoder.findElementEnd(b.buffer.subarray(0,b.bufferOffset))){3<LOG&&console.log("Incomplete packet received. Length "+e.length+". Wait for more input.");
-return}3<LOG&&console.log("Complete packet received. Length "+e.length+". Start decoding.")}catch(f){console.log("NDN.ws.onmessage exception: "+f);return}c=new BinaryXMLDecoder(b.buffer);if(c.peekStartElement(CCNProtocolDTags.Interest)){3<LOG&&console.log("Interest packet received.");e=new Interest;e.from_ccnb(c);3<LOG&&console.log(e);var g=escape(e.name.getName());3<LOG&&console.log(g);g=getEntryForRegisteredPrefix(g);null!=g&&(e=new UpcallInfo(a,e,0,null),g.closure.upcall(Closure.UPCALL_INTEREST,
-e)==Closure.RESULT_INTEREST_CONSUMED&&null!=e.contentObject&&(g=encodeToBinaryContentObject(e.contentObject),e=new Uint8Array(g.length),e.set(g),b.ws.send(e.buffer)))}else if(c.peekStartElement(CCNProtocolDTags.ContentObject)){3<LOG&&console.log("ContentObject packet received.");e=new ContentObject;e.from_ccnb(c);3<LOG&&console.log(e);g=e.name.getName();console.log(g);if(e.signedInfo&&e.signature){3<LOG&&console.log("Key verification...");var d=DataUtils.toHex(e.signature.signature).toLowerCase(),
-h=e.signedInfo.locator;if(h.type==KeyLocatorType.KEYNAME)console.log("KeyLocator contains KEYNAME"),d=h.keyName.contentName.getName(),console.log(d);else if(h.type==KeyLocatorType.KEY){console.log("Keylocator contains KEY");var j=DataUtils.toHex(e.signedInfo.locator.publicKey).toLowerCase(),h=j.slice(56,314),j=j.slice(318,324),k=new RSAKey;k.setPublic(h,j);(d=k.verifyByteArray(e.rawSignatureData,d))?console.log("SIGNATURE VALID"):console.log("SIGNATURE INVALID")}else d=h.certificate,console.log("KeyLocator contains CERT"),
-console.log(d)}null==b.ccndid&&null!=g.match(NDN.ccndIdFetcher)?!e.signedInfo||!e.signedInfo.publisher||!e.signedInfo.publisher.publisherPublicKeyDigest?(console.log("Cannot contact router, close NDN now."),a.readyStatus=NDN.CLOSED,a.onclose()):(b.ccndid=e.signedInfo.publisher.publisherPublicKeyDigest,3<LOG&&console.log(b.ccndid),a.readyStatus=NDN.OPENED,a.onopen()):(g=NDN.getEntryForExpressedInterest(e.name),null!=g&&(clearTimeout(g.closure.timerID),d=NDN.PITTable.indexOf(g),0<=d&&NDN.PITTable.splice(d,
-1),g.closure.upcall(Closure.UPCALL_CONTENT,new UpcallInfo(a,null,0,e))))}else console.log("Incoming packet is not Interest or ContentObject. Discard now.");delete c;delete b.structureDecoder;delete b.buffer;b.structureDecoder=new BinaryXMLStructureDecoder;b.buffer=new Uint8Array(b.maxBufferSize);b.bufferOffset=0}};this.ws.onopen=function(a){3<LOG&&console.log(a);3<LOG&&console.log("ws.onopen: WebSocket connection opened.");3<LOG&&console.log("ws.onopen: ReadyState: "+this.readyState);a=new Interest(new Name(NDN.ccndIdFetcher));
-a.interestLifetime=4;var a=encodeToBinaryInterest(a),d=new Uint8Array(a.length);d.set(a);b.ws.send(d.buffer)};this.ws.onerror=function(a){console.log("ws.onerror: ReadyState: "+this.readyState);console.log(a);console.log("ws.onerror: WebSocket error: "+a.data)};this.ws.onclose=function(){console.log("ws.onclose: WebSocket connection closed.");b.ws=null;a.readyStatus=NDN.CLOSED;a.onclose()}};
+WebSocketTransport.prototype.connectWebSocket=function(a){null!=this.ws&&delete this.ws;this.ws=new WebSocket("ws://"+a.host+":"+a.port);0<LOG&&console.log("ws connection created.");this.ws.binaryType="arraybuffer";var b=this;this.ws.onmessage=function(c){c=c.data;if(null==c||void 0==c||""==c)console.log("INVALID ANSWER");else if(c instanceof ArrayBuffer){var d=new Uint8Array(c);3<LOG&&console.log("BINARY RESPONSE IS "+DataUtils.toHex(d));try{if(d.length+b.bufferOffset>=b.buffer.byteLength){3<LOG&&
+console.log("NDN.ws.onmessage: buffer overflow. Accumulate received length: "+b.bufferOffset+". Current packet length: "+d.length+".");delete b.structureDecoder;delete b.buffer;b.structureDecoder=new BinaryXMLStructureDecoder;b.buffer=new Uint8Array(b.maxBufferSize);b.bufferOffset=0;return}b.buffer.set(d,b.bufferOffset);b.bufferOffset+=d.length;if(!b.structureDecoder.findElementEnd(b.buffer.subarray(0,b.bufferOffset))){3<LOG&&console.log("Incomplete packet received. Length "+d.length+". Wait for more input.");
+return}3<LOG&&console.log("Complete packet received. Length "+d.length+". Start decoding.")}catch(e){console.log("NDN.ws.onmessage exception: "+e);return}c=new BinaryXMLDecoder(b.buffer);if(c.peekStartElement(CCNProtocolDTags.Interest)){3<LOG&&console.log("Interest packet received.");d=new Interest;d.from_ccnb(c);3<LOG&&console.log(d);var f=escape(d.name.getName());3<LOG&&console.log(f);var g=getEntryForRegisteredPrefix(f);null!=g&&(d=new UpcallInfo(a,d,0,null),g.closure.upcall(Closure.UPCALL_INTEREST,
+d)==Closure.RESULT_INTEREST_CONSUMED&&null!=d.contentObject&&(g=encodeToBinaryContentObject(d.contentObject),d=new Uint8Array(g.length),d.set(g),b.ws.send(d.buffer)))}else if(c.peekStartElement(CCNProtocolDTags.ContentObject))if(console.log("ContentObject packet received."),d=new ContentObject,d.from_ccnb(c),3<LOG&&console.log(d),f=d.name.getName(),console.log(f),null==b.ccndid&&null!=f.match(NDN.ccndIdFetcher))!d.signedInfo||!d.signedInfo.publisher||!d.signedInfo.publisher.publisherPublicKeyDigest?
+(console.log("Cannot contact router, close NDN now."),a.readyStatus=NDN.CLOSED,a.onclose()):(b.ccndid=d.signedInfo.publisher.publisherPublicKeyDigest,3<LOG&&console.log(b.ccndid),a.readyStatus=NDN.OPENED,a.onopen());else{if(g=NDN.getEntryForExpressedInterest(d.name),null!=g){var h=NDN.PITTable.indexOf(g);0<=h&&NDN.PITTable.splice(h,1);g=g.closure;clearTimeout(g.timerID);var j=!1,k=function(a,b,c,d){this.contentObject=a;this.closure=b;this.keyName=c;this.signature=d;Closure.call(this)};k.prototype.upcall=
+function(b,c){if(b==Closure.UPCALL_INTEREST_TIMED_OUT)console.log("In KeyFetchClosure.upcall: interest time out.");else if(b==Closure.UPCALL_CONTENT){console.log("In KeyFetchClosure.upcall");var d=DataUtils.toHex(c.contentObject.content).toLowerCase();console.log("Key: "+d);var e=d.slice(56,314),d=d.slice(318,324),f=new RSAKey;f.setPublic(e,d);j=f.verifyByteArray(this.contentObject.rawSignatureData,n);e=!0==j?Closure.UPCALL_CONTENT:Closure.UPCALL_CONTENT_BAD;console.log("raise encapsulated closure");
+this.closure.upcall(e,new UpcallInfo(a,null,0,this.contentObject))}};if(d.signedInfo&&d.signedInfo.locator&&d.signature){3<LOG&&console.log("Key verification...");var n=DataUtils.toHex(d.signature.signature).toLowerCase(),h=d.signedInfo.locator;if(h.type==KeyLocatorType.KEYNAME){console.log("KeyLocator contains KEYNAME");var m=h.keyName.contentName.getName();console.log(m);f.match("/ccnx.org/Users/")?(console.log("Key found"),g.upcall(Closure.UPCALL_CONTENT,new UpcallInfo(a,null,0,d))):(console.log("Fetch key according to keylocator"),
+g=new k(d,g,m,n),d=new Interest(h.keyName.contentName.getPrefix(4)),d.interestLifetime=4,b.expressInterest(a,d,g))}else h.type==KeyLocatorType.KEY?(console.log("Keylocator contains KEY"),h=DataUtils.toHex(d.signedInfo.locator.publicKey).toLowerCase(),console.log(h),f=h.slice(56,314),h=h.slice(318,324),k=new RSAKey,k.setPublic(f,h),j=k.verifyByteArray(d.rawSignatureData,n),g.upcall(Closure.UPCALL_CONTENT,new UpcallInfo(a,null,0,d))):(d=h.certificate,console.log("KeyLocator contains CERT"),console.log(d))}}}else console.log("Incoming packet is not Interest or ContentObject. Discard now.");
+delete c;delete b.structureDecoder;delete b.buffer;b.structureDecoder=new BinaryXMLStructureDecoder;b.buffer=new Uint8Array(b.maxBufferSize);b.bufferOffset=0}};this.ws.onopen=function(a){3<LOG&&console.log(a);3<LOG&&console.log("ws.onopen: WebSocket connection opened.");3<LOG&&console.log("ws.onopen: ReadyState: "+this.readyState);a=new Interest(new Name(NDN.ccndIdFetcher));a.interestLifetime=4;var a=encodeToBinaryInterest(a),d=new Uint8Array(a.length);d.set(a);b.ws.send(d.buffer)};this.ws.onerror=
+function(a){console.log("ws.onerror: ReadyState: "+this.readyState);console.log(a);console.log("ws.onerror: WebSocket error: "+a.data)};this.ws.onclose=function(){console.log("ws.onclose: WebSocket connection closed.");b.ws=null;a.readyStatus=NDN.CLOSED;a.onclose()}};
 WebSocketTransport.prototype.expressInterest=function(a,b,c){if(null!=this.ws){var d=encodeToBinaryInterest(b),e=new Uint8Array(d.length);e.set(d);var f=new PITEntry(b,c);NDN.PITTable.push(f);this.ws.send(e.buffer);3<LOG&&console.log("ws.send() returned.");c.timerID=setTimeout(function(){3<LOG&&console.log("Interest time out.");var d=NDN.PITTable.indexOf(f);0<=d&&NDN.PITTable.splice(d,1);c.upcall(Closure.UPCALL_INTEREST_TIMED_OUT,new UpcallInfo(a,b,0,null))},1E3*b.interestLifetime)}else console.log("WebSocket connection is not established.")};
 var CSTable=[],CSEntry=function(a,b){this.name=a;this.closure=b};function getEntryForRegisteredPrefix(a){for(var b=0;b<CSTable.length;b++)if(null!=CSTable[b].name.match(a))return CSTable[b];return null}
 WebSocketTransport.prototype.registerPrefix=function(a,b,c){if(null!=this.ws){if(null==this.ccndid)return console.log("ccnd node ID unkonwn. Cannot register prefix."),-1;var a=new ForwardingEntry("selfreg",b,null,null,3,2147483647),a=encodeForwardingEntry(a),d=new SignedInfo;d.setFields();a=new ContentObject(new Name,d,a,new Signature);a.sign();a=encodeToBinaryContentObject(a);a=new Name(["ccnx",this.ccndid,"selfreg",a]);a=new Interest(a);a.scope=1;d=encodeToBinaryInterest(a);a=new Uint8Array(d.length);