Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ sudo: false
language: node_js
node_js:
- "6.0"
env:
- CXX=g++-4.8

cache:
directories:
- node_modules
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
hosts:
- nic.localhost
- tim.localhost
- nicola.localhost

cache:
directories:
- node_modules
6 changes: 6 additions & 0 deletions bin/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ module.exports = [
return answers.webid
}
},
{
name: 'acceptCertificateHeader',
question: 'Accept client certificates through the X-SSL-Cert header (for reverse proxies)',
default: false,
prompt: false
},
{
name: 'useOwner',
question: 'Do you already have a WebID?',
Expand Down
54 changes: 50 additions & 4 deletions lib/api/authn/webid-tls.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
var webid = require('webid/tls')
var debug = require('../../debug').authentication
var x509 // optional dependency, load lazily

const CERTIFICATE_MATCHER = /^-----BEGIN CERTIFICATE-----\n(?:[A-Za-z0-9+/=]+\n)+-----END CERTIFICATE-----$/m

function authenticate () {
return handler
Expand All @@ -13,10 +16,9 @@ function handler (req, res, next) {
return next()
}

var certificate = req.connection.getPeerCertificate()
// Certificate is empty? skip
if (certificate === null || Object.keys(certificate).length === 0) {
debug('No client certificate found in the request. Did the user click on a cert?')
// No certificate? skip
const certificate = getCertificateViaTLS(req) || getCertificateViaHeader(req)
if (!certificate) {
setEmptySession(req)
return next()
}
Expand All @@ -36,6 +38,50 @@ function handler (req, res, next) {
})
}

// Tries to obtain a client certificate retrieved through the TLS handshake
function getCertificateViaTLS (req) {
const certificate = req.connection.getPeerCertificate &&
req.connection.getPeerCertificate()
if (certificate && Object.keys(certificate).length > 0) {
return certificate
}
debug('No peer certificate received during TLS handshake.')
}

// Tries to obtain a client certificate retrieved through the X-SSL-Cert header
function getCertificateViaHeader (req) {
// Only allow the X-SSL-Cert header if explicitly enabled
if (!req.app.locals.acceptCertificateHeader) return

// Try to retrieve the certificate from the header
const header = req.headers['x-ssl-cert']
if (!header) {
return debug('No certificate received through the X-SSL-Cert header.')
}
// The certificate's newlines have been replaced by tabs
// in order to fit in an HTTP header (NGINX does this automatically)
const rawCertificate = header.replace(/\t/g, '\n')

// Ensure the header contains a valid certificate
// (x509 unsafely interprets it as a file path otherwise)
if (!CERTIFICATE_MATCHER.test(rawCertificate)) {
return debug('Invalid value for the X-SSL-Cert header.')
}

// Parse and convert the certificate to the format the webid library expects
if (!x509) x509 = require('x509')
try {
const { publicKey, extensions } = x509.parseCert(rawCertificate)
return {
modulus: publicKey.n,
exponent: '0x' + parseInt(publicKey.e, 10).toString(16),
subjectaltname: extensions && extensions.subjectAlternativeName
}
} catch (error) {
debug('Invalid certificate received through the X-SSL-Cert header.')
}
}

function setEmptySession (req) {
req.session.userId = ''
req.session.identified = false
Expand Down
1 change: 1 addition & 0 deletions lib/create-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ function initAuthentication (argv, app) {
case 'tls':
// Enforce authentication with WebID-TLS on all LDP routes
app.use('/', API.tls.authenticate())
app.locals.acceptCertificateHeader = argv.acceptCertificateHeader
break
case 'oidc':
let oidc = OidcManager.fromServerConfig(argv)
Expand Down
10 changes: 7 additions & 3 deletions lib/create-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function createServer (argv, app) {
argv = argv || {}
app = app || express()
var ldpApp = createApp(argv)
var ldp = ldpApp.locals.ldp
var ldp = ldpApp.locals.ldp || {}
var mount = argv.mount || '/'
// Removing ending '/'
if (mount.length > 1 &&
Expand All @@ -21,9 +21,13 @@ function createServer (argv, app) {
}
app.use(mount, ldpApp)
debug.settings('Base URL (--mount): ' + mount)
var server = http.createServer(app)

if (ldp && (ldp.webid || ldp.idp || argv.sslKey || argv.sslCert)) {
var server
var needsTLS = argv.sslKey || argv.sslCert ||
(ldp.webid || ldp.idp) && !argv.acceptCertificateHeader
if (!needsTLS) {
server = http.createServer(app)
} else {
debug.settings('SSL Private Key path: ' + argv.sslKey)
debug.settings('SSL Certificate path: ' + argv.sslCert)

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"vhost": "^3.0.2",
"webid": "^0.3.7"
},
"optionalDependencies": {
"x509": "^0.3.2"
},
"devDependencies": {
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
Expand Down
163 changes: 130 additions & 33 deletions test/integration/acl-tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,43 @@ var rm = require('../test-utils').rm
var ldnode = require('../../index')
var ns = require('solid-namespace')($rdf)

describe('ACL HTTP', function () {
var address = 'https://localhost:3456/test/'
let rootPath = path.join(__dirname, '../resources')

var aclExtension = '.acl'
var metaExtension = '.meta'

var testDir = 'acl-tls/testDir'
var testDirAclFile = testDir + '/' + aclExtension
var testDirMetaFile = testDir + '/' + metaExtension

var abcFile = testDir + '/abc.ttl'
var abcAclFile = abcFile + aclExtension

var globFile = testDir + '/*'

var groupFile = testDir + '/group'

var origin1 = 'http://example.org/'
var origin2 = 'http://example.com/'

var user1 = 'https://user1.databox.me/profile/card#me'
var user2 = 'https://user2.databox.me/profile/card#me'
var userCredentials = {
user1: {
cert: fs.readFileSync(path.join(__dirname, '../keys/user1-cert.pem')),
key: fs.readFileSync(path.join(__dirname, '../keys/user1-key.pem'))
},
user2: {
cert: fs.readFileSync(path.join(__dirname, '../keys/user2-cert.pem')),
key: fs.readFileSync(path.join(__dirname, '../keys/user2-key.pem'))
}
}

describe('ACL with WebID+TLS', function () {
this.timeout(10000)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'

var address = 'https://localhost:3456/test/'
let rootPath = path.join(__dirname, '../resources')
var ldpHttpsServer
var ldp = ldnode.createServer({
mount: '/test',
Expand All @@ -45,36 +76,6 @@ describe('ACL HTTP', function () {
fs.removeSync(path.join(rootPath, 'index.html.acl'))
})

var aclExtension = '.acl'
var metaExtension = '.meta'

var testDir = 'acl-tls/testDir'
var testDirAclFile = testDir + '/' + aclExtension
var testDirMetaFile = testDir + '/' + metaExtension

var abcFile = testDir + '/abc.ttl'
var abcAclFile = abcFile + aclExtension

var globFile = testDir + '/*'

var groupFile = testDir + '/group'

var origin1 = 'http://example.org/'
var origin2 = 'http://example.com/'

var user1 = 'https://user1.databox.me/profile/card#me'
var user2 = 'https://user2.databox.me/profile/card#me'
var userCredentials = {
user1: {
cert: fs.readFileSync(path.join(__dirname, '../keys/user1-cert.pem')),
key: fs.readFileSync(path.join(__dirname, '../keys/user1-key.pem'))
},
user2: {
cert: fs.readFileSync(path.join(__dirname, '../keys/user2-cert.pem')),
key: fs.readFileSync(path.join(__dirname, '../keys/user2-key.pem'))
}
}

function createOptions (path, user) {
var options = {
url: address + path,
Expand Down Expand Up @@ -971,3 +972,99 @@ describe('ACL HTTP', function () {
})
})
})

describe('ACL with WebID through X-SSL-Cert', function () {
this.timeout(10000)

var ldpHttpsServer
before(function (done) {
const ldp = ldnode.createServer({
mount: '/test',
root: rootPath,
webid: true,
auth: 'tls',
acceptCertificateHeader: true
})
ldpHttpsServer = ldp.listen(3456, done)
})

after(function () {
if (ldpHttpsServer) ldpHttpsServer.close()
fs.removeSync(path.join(rootPath, 'index.html'))
fs.removeSync(path.join(rootPath, 'index.html.acl'))
})

function prepareRequest (certHeader, setResponse) {
return done => {
const options = {
url: address.replace('https', 'http') + '/acl-tls/write-acl/.acl',
headers: { 'X-SSL-Cert': certHeader }
}
request(options, function (error, response) {
setResponse(response)
done(error)
})
}
}

describe('without certificate', function () {
var response
before(prepareRequest('', res => { response = res }))

it('should return 401', function () {
assert.propertyVal(response, 'statusCode', 401)
})
})

describe('with a valid certificate', function () {
// Escape certificate for usage in HTTP header
const escapedCert = userCredentials.user1.cert.toString()
.replace(/\n/g, '\t')

var response
before(prepareRequest(escapedCert, res => { response = res }))

it('should return 200', function () {
assert.propertyVal(response, 'statusCode', 200)
})

it('should set the User header', function () {
assert.propertyVal(response.headers, 'user', 'https://user1.databox.me/profile/card#me')
})
})

describe('with a local filename as certificate', function () {
const certFile = path.join(__dirname, '../keys/user1-cert.pem')

var response
before(prepareRequest(certFile, res => { response = res }))

it('should return 401', function () {
assert.propertyVal(response, 'statusCode', 401)
})
})

describe('with an invalid certificate value', function () {
var response
before(prepareRequest('xyz', res => { response = res }))

it('should return 401', function () {
assert.propertyVal(response, 'statusCode', 401)
})
})

describe('with an invalid certificate', function () {
const invalidCert =
`-----BEGIN CERTIFICATE-----
ABCDEF
-----END CERTIFICATE-----`
.replace(/\n/g, '\t')

var response
before(prepareRequest(invalidCert, res => { response = res }))

it('should return 401', function () {
assert.propertyVal(response, 'statusCode', 401)
})
})
})