var forge = require('../js/forge'); var fs = require('fs'); var http = require('http'); //var rdf = require('./rdflib'); var sys = require('sys'); var urllib = require('url'); var ws = require('./ws'); // remove xmlns from input var normalizeNs = function(input, ns) { var rval = null; // primitive if(typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean') { rval = input; } // array else if(forge.util.isArray(input)) { rval = []; for(var i = 0; i < input.length; ++i) { rval.push(normalizeNs(input[i], ns)); } } // object else { if('@' in input) { // copy namespace map var newNs = {}; for(var key in ns) { newNs[key] = ns[key]; } ns = newNs; // update namespace map for(var key in input['@']) { if(key.indexOf('xmlns:') === 0) { ns[key.substr(6)] = input['@'][key]; } } } rval = {}; for(var key in input) { if(key.indexOf('xmlns:') !== 0) { var value = input[key]; var colon = key.indexOf(':'); if(colon !== -1) { var prefix = key.substr(0, colon); if(prefix in ns) { key = ns[prefix] + key.substr(colon + 1); } } rval[key] = normalizeNs(value, ns); } } } return rval; }; // gets public key from WebID rdf var getPublicKey = function(data, uri, callback) { // FIXME: use RDF library to simplify code below //var kb = new rdf.RDFParser(rdf.IndexedFormula(), uri).loadBuf(data); //var CERT = rdf.Namespace('http://www.w3.org/ns/auth/cert#'); //var RSA = rdf.Namespace('http://www.w3.org/ns/auth/rsa#'); var RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; var CERT = 'http://www.w3.org/ns/auth/cert#'; var RSA = 'http://www.w3.org/ns/auth/rsa#'; var desc = RDF + 'Description'; var about = RDF + 'about'; var type = RDF + 'type'; var resource = RDF + 'resource'; var publicKey = RSA + 'RSAPublicKey'; var modulus = RSA + 'modulus'; var exponent = RSA + 'public_exponent'; var identity = CERT + 'identity'; var hex = CERT + 'hex'; var decimal = CERT + 'decimal'; // gets a resource identifer from a node var getResource = function(node, key) { var rval = null; // special case 'about' if(key === about) { if('@' in node && about in node['@']) { rval = node['@'][about]; } } // any other resource else if( key in node && typeof node[key] === 'object' && !forge.util.isArray(node[key]) && '@' in node[key] && resource in node[key]['@']) { rval = node[key]['@'][resource]; } return rval; }; // parse XML uri = urllib.parse(uri); var xml2js = require('./xml2js'); var parser = new xml2js.Parser(); parser.addListener('end', function(result) { // normalize namespaces result = normalizeNs(result, {}); // find grab all public keys whose identity matches hash from uri var keys = []; if(desc in result) { // normalize RDF descriptions to array if(!forge.util.isArray(result[desc])) { desc = [result[desc]]; } else { desc = result[desc]; } // collect properties for all resources var graph = {}; for(var i = 0; i < desc.length; ++i) { var node = desc[i]; var res = {}; for(var key in node) { var obj = getResource(node, key); res[key] = (obj === null) ? node[key] : obj; } graph[getResource(node, about) || ''] = res; } // for every public key w/identity that matches the uri hash // save the public key modulus and exponent for(var r in graph) { var props = graph[r]; if(identity in props && type in props && props[type] === publicKey && props[identity] === uri.hash && modulus in props && exponent in props && props[modulus] in graph && props[exponent] in graph && hex in graph[props[modulus]] && decimal in graph[props[exponent]]) { keys.push({ modulus: graph[props[modulus]][hex], exponent: graph[props[exponent]][decimal] }); } } } sys.log('Public keys from RDF: ' + JSON.stringify(keys)); callback(keys); }); parser.parseString(data); }; // compares two public keys for equality var comparePublicKeys = function(key1, key2) { return key1.modulus === key2.modulus && key1.exponent === key2.exponent; }; // gets the RDF data from a URL var fetchUrl = function(url, callback, redirects) { // allow 3 redirects by default if(typeof(redirects) === 'undefined') { redirects = 3; } sys.log('Fetching URL: \"' + url + '\"'); // parse URL url = forge.util.parseUrl(url); var client = http.createClient( url.port, url.fullHost, url.scheme === 'https'); var request = client.request('GET', url.path, { 'Host': url.host, 'Accept': 'application/rdf+xml' }); request.addListener('response', function(response) { var body = ''; // error, return empty body if(response.statusCode >= 400) { callback(body); } // follow redirect else if(response.statusCode === 302) { if(redirects > 0) { // follow redirect fetchUrl(response.headers.location, callback, --redirects); } else { // return empty body callback(body); } } // handle data else { response.setEncoding('utf8'); response.addListener('data', function(chunk) { body += chunk; }); response.addListener('end', function() { callback(body); }); } }); request.end(); }; // does WebID authentication var authenticateWebId = function(c, state) { var auth = false; // get client-certificate var cert = c.peerCertificate; // get public key from certificate var publicKey = { modulus: cert.publicKey.n.toString(16).toLowerCase(), exponent: cert.publicKey.e.toString(10) }; sys.log( 'Server verifying certificate w/CN: \"' + cert.subject.getField('CN').value + '\"\n' + 'Public Key: ' + JSON.stringify(publicKey)); // build queue of subject alternative names to authenticate with var altNames = []; var ext = cert.getExtension({name: 'subjectAltName'}); if(ext !== null && ext.altNames) { for(var i = 0; i < ext.altNames.length; ++i) { var altName = ext.altNames[i]; if(altName.type === 6) { altNames.push(altName.value); } } } // create authentication processor var authNext = function() { if(!auth) { // no more alt names, auth failed if(altNames.length === 0) { sys.log('WebID authentication FAILED.'); c.prepare(JSON.stringify({ success: false, error: 'Not Authenticated' })); c.close(); } // try next alt name else { // fetch URL var url = altNames.shift(); fetchUrl(url, function(body) { // get public key getPublicKey(body, url, function(keys) { // compare public keys from RDF until one matches for(var i = 0; !auth && i < keys.length; ++i) { auth = comparePublicKeys(keys[i], publicKey); } if(auth) { // send authenticated notice to client sys.log('WebID authentication PASSED.'); state.authenticated = true; c.prepare(JSON.stringify({ success: true, cert: forge.pki.certificateToPem(cert), webID: url, rdf: forge.util.encode64(body) })); } else { // try next alt name authNext(); } }); }); } } }; // do auth authNext(); }; // creates credentials (private key + certificate) var createCredentials = function(cn, credentials) { sys.log('Generating 512-bit key-pair and certificate for \"' + cn + '\".'); var keys = forge.pki.rsa.generateKeyPair(512); sys.log('key-pair created.'); var cert = forge.pki.createCertificate(); cert.serialNumber = '01'; cert.validity.notBefore = new Date(); cert.validity.notAfter = new Date(); cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); var attrs = [{ name: 'commonName', value: cn }, { name: 'countryName', value: 'US' }, { shortName: 'ST', value: 'Virginia' }, { name: 'localityName', value: 'Blacksburg' }, { name: 'organizationName', value: 'Test' }, { shortName: 'OU', value: 'Test' }]; cert.setSubject(attrs); cert.setIssuer(attrs); cert.setExtensions([{ name: 'basicConstraints', cA: true }, { name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true }, { name: 'subjectAltName', altNames: [{ type: 6, // URI value: 'http://myuri.com/webid#me' }] }]); // FIXME: add subjectKeyIdentifier extension // FIXME: add authorityKeyIdentifier extension cert.publicKey = keys.publicKey; // self-sign certificate cert.sign(keys.privateKey); // save credentials credentials.key = forge.pki.privateKeyToPem(keys.privateKey); credentials.cert = forge.pki.certificateToPem(cert); sys.log('Certificate created for \"' + cn + '\": \n' + credentials.cert); }; // initialize credentials var credentials = { key: null, cert: null }; // read private key from file var readPrivateKey = function(filename) { credentials.key = fs.readFileSync(filename); // try to parse from PEM as test forge.pki.privateKeyFromPem(credentials.key); }; // read certificate from file var readCertificate = function(filename) { credentials.cert = fs.readFileSync(filename); // try to parse from PEM as test forge.pki.certificateFromPem(credentials.cert); }; // parse command line options var opts = require('opts'); var options = [ { short : 'v' , long : 'version' , description : 'Show version and exit' , callback : function() { console.log('v1.0'); process.exit(1); } }, { short : 'p' , long : 'port' , description : 'The port to listen for WebSocket connections on' , value : true }, { long : 'key' , description : 'The server private key file to use in PEM format' , value : true , callback : readPrivateKey }, { long : 'cert' , description : 'The server certificate file to use in PEM format' , value : true , callback : readCertificate } ]; opts.parse(options, true); // create credentials for server if(credentials.key === null || credentials.cert === null) { createCredentials('server', credentials); } // function to create TLS server connection var createTls = function(websocket) { var state = { authenticated: false }; return forge.tls.createConnection({ server: true, caStore: [], sessionCache: {}, // supported cipher suites in order of preference cipherSuites: [ forge.tls.CipherSuites.TLS_RSA_WITH_AES_128_CBC_SHA, forge.tls.CipherSuites.TLS_RSA_WITH_AES_256_CBC_SHA], connected: function(c) { sys.log('Server connected'); // do WebID authentication try { authenticateWebId(c, state); } catch(ex) { c.close(); } }, verifyClient: true, verify: function(c, verified, depth, certs) { // accept certs w/unknown-CA (48) if(verified === 48) { verified = true; } return verified; }, getCertificate: function(c, hint) { sys.log('Server using certificate for \"' + hint[0] + '\"'); return credentials.cert; }, getPrivateKey: function(c, cert) { return credentials.key; }, tlsDataReady: function(c) { // send base64-encoded TLS data over websocket websocket.write(forge.util.encode64(c.tlsData.getBytes())); }, dataReady: function(c) { // ignore any data until connection is authenticated if(state.authenticated) { sys.log('Server received \"' + c.data.getBytes() + '\"'); } }, closed: function(c) { sys.log('Server disconnected'); websocket.end(); }, error: function(c, error) { sys.log('Server error: ' + error.message); } }); }; // create websocket server var port = opts.get('port') || 8080; ws.createServer(function(websocket) { // create TLS server connection var tls = createTls(websocket); websocket.addListener('connect', function(resource) { sys.log('WebSocket connected: ' + resource); // close connection after 30 second timeout setTimeout(websocket.end, 30 * 1000); }); websocket.addListener('data', function(data) { // base64-decode data and process it tls.process(forge.util.decode64(data)); }); websocket.addListener('close', function() { sys.log('WebSocket closed'); }); }).listen(port); sys.log('WebSocket WebID server running on port ' + port);