blob: fae0b82541891f88a6e19dcffac8f7a652f6c7bc [file] [log] [blame]
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);