492 lines
13 KiB
JavaScript
492 lines
13 KiB
JavaScript
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);
|