now generates certs from a signToken and public key

This commit is contained in:
Zachary Carter 2013-05-16 17:13:01 -07:00
Родитель c0ffe969f8
Коммит 4990878d19
11 изменённых файлов: 270 добавлений и 12 удалений

1
packages/fxa-auth-server/.gitignore поставляемый
Просмотреть файл

@ -1,2 +1,3 @@
/node_modules
/config/*.json
*.swp

Просмотреть файл

@ -21,3 +21,7 @@ env:
before_script:
- "mysql -e 'DROP DATABASE IF EXISTS test;'"
- "mysql -e 'CREATE DATABASE test;'"
- ./scripts/gen_keys.js
before_install:
- sudo apt-get install libgmp3-dev

Просмотреть файл

@ -9,9 +9,10 @@ You'll need node 0.10.x or higher and npm to run the server.
Clone the git repository and install dependencies:
git://github.com/mozilla/picl-idp.git
git clone git://github.com/mozilla/picl-idp.git
cd picl-idp
npm install
node ./scripts/gen_keys.js
To start the server, run:

Просмотреть файл

@ -137,6 +137,43 @@ exports.finishLogin = function(sessionId, verifier, cb) {
], cb);
};
// Takes an accountToken and creates a new signToken
exports.getSignToken = function(accountToken, cb) {
var accountKey = accountToken + '/accountToken';
var uid, signToken;
async.waterfall([
// Check that the accountToken exists
// and get the associated user id
function(cb) {
kv.get(accountKey, function(err, account) {
if (err) return cb(err);
if (!account) return cb(notFound('UknownAccountToken'));
cb(null, account.value.uid);
});
},
// get new signToken
function(id, cb) {
uid = id;
util.getSignToken(cb);
},
function(token, cb) {
signToken = token;
kv.set(token + '/signer', {
uid: uid,
accessTime: Date.now()
}, cb);
},
// delete accountToken
function(cb) {
kv.delete(accountToken + '/accountToken', cb);
},
function(cb) {
cb(null, { signToken: signToken });
}
], cb);
};
// This method returns the userId currently associated with an email address.
exports.getId = function(email, cb) {
kv.get(email + '/uid', function(err, result) {
@ -155,3 +192,17 @@ exports.getUser = function(userId, cb) {
});
};
// This account principle associated with a singing token
// The principle is the userId combined with the IDP domain
// e.g. 1234@lcip.org
//
exports.getPrinciple = function(token, cb) {
kv.get(token + '/signer', function(err, result) {
if (err) return cb(internalError(err));
if (!result) return cb(notFound('UnknownSignToken'));
var principle = result.value.uid + '@' + config.get('domain');
cb(null, principle);
});
};

Просмотреть файл

@ -23,7 +23,13 @@ var conf = module.exports = convict({
},
domain: {
format: "url",
default: "http://127.0.0.1:9000"
default: "127.0.0.1:9000"
},
secretKeyFile: {
default: "./config/secret-key.json"
},
publicKeyFile: {
default: "./config/public-key.json"
},
kvstore: {
backend: {
@ -106,7 +112,7 @@ if (process.env.CONFIG_FILES) {
}
// set the public url as the issuer domain for assertions
conf.set('domain', url.parse(conf.get('public_url')).hostname);
conf.set('domain', url.parse(conf.get('public_url')).host);
if (conf.get('env') === 'test') {
if (conf.get('kvstore.backend') === 'mysql') {
@ -114,6 +120,12 @@ if (conf.get('env') === 'test') {
}
}
const configDir = fs.realpathSync(__dirname + "/../config");
const pubKeyFile = configDir + "/public-key.json";
const secretKeyFile = configDir + "/secret-key.json";
conf.set('secretKeyFile', secretKeyFile);
conf.set('publicKeyFile', pubKeyFile);
conf.validate();
console.log('configuration: ', conf.toString());

Просмотреть файл

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Hapi = require('hapi');
const account = require('./account');
// Prerequesites can be included in a route's configuration and will run
// before the route's handler is called. Results are set on
// the request.pre object using the method's name for the property name,
// or otherwise using the value of the "assign" property.
//
// Methods specified as strings are helpers. Check ./helpers.js for
// definitions.
module.exports = {
principle: {
method: function(request, next) {
if(request.payload.email) return next(request.payload.email);
var token = request.payload.token;
if (!token) next(Hapi.Error.badRequest('MissingSignToken'));
account.getPrinciple(token, function(err, principle) {
if (err) next(err);
else next(principle);
});
},
assign: 'principle'
}
};

Просмотреть файл

@ -19,6 +19,12 @@ function getAccountToken(cb) {
});
}
function getSignToken(cb) {
return crypto.randomBytes(32, function(err, buf) {
cb(null, buf.toString('hex'));
});
}
function getUserId() {
return uuid.v4();
}
@ -32,5 +38,6 @@ module.exports = {
getDeviceId: getDeviceId,
getUserId: getUserId,
getSessionId: getSessionId,
getAccountToken: getAccountToken
getAccountToken: getAccountToken,
getSignToken: getSignToken
};

Просмотреть файл

@ -3,8 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Hapi = require('hapi');
const fs = require('fs');
const CC = require('compute-cluster');
const config = require('../lib/config');
const prereqs = require('../lib/prereqs');
const hour = 1000 * 60 * 60;
@ -58,12 +60,13 @@ var routes = [
path: '/sign',
config: {
handler: sign,
pre: [ prereqs.principle ],
validate: {
payload: {
email: Hapi.types.String().required(), // for testing only
email: Hapi.types.String().without('token'), // for testing only
publicKey: Hapi.types.String().required(),
duration: Hapi.types.Number().integer().min(0).max(24 * hour).required(),
token: Hapi.types.String()
token: Hapi.types.String().without('email')
}
}
}
@ -92,12 +95,29 @@ var routes = [
}
}
}
},
{
method: 'POST',
path: '/signToken',
config: {
handler: getSignToken,
validate: {
payload: {
accountToken: Hapi.types.String().required()
},
response: {
schema: {
signToken: Hapi.types.String().required()
}
}
}
}
}
];
function wellKnown(request) {
request.reply({
'public-key': config.idpPublicKey,
'public-key': fs.readFileSync(config.get('publicKeyFile')),
'authentication': '/sign_in.html',
'provisioning': '/provision.html'
});
@ -122,9 +142,14 @@ function create(request) {
}
function sign(request) {
// TODO validate token, get email from token
var principle = request.pre.principle;
cc.enqueue(
request.payload,
{
email: principle,
publicKey: request.payload.publicKey,
duration: request.payload.duration
},
function (err, result) {
if (err || result.err) {
request.reply(Hapi.error.internal('Unable to sign certificate', err || result.err));
@ -169,6 +194,21 @@ function finishLogin(request) {
}
function getSignToken(request) {
account.getSignToken(
request.payload.accountToken,
function (err, result) {
if (err) {
request.reply(err);
}
else {
request.reply(result);
}
}
);
}
module.exports = {
routes: routes
};

Просмотреть файл

@ -2,12 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const fs = require('fs');
const jwcrypto = require('jwcrypto');
const config = require('../lib/config');
require('jwcrypto/lib/algs/rs');
require('jwcrypto/lib/algs/ds');
const _privKey = jwcrypto.loadSecretKey(fs.readFileSync(config.get('secretKeyFile')));
process.on('message', function (message) {
var clientKey = jwcrypto.loadPublicKey(message.publicKey);
var now = Date.now();
@ -19,12 +22,12 @@ process.on('message', function (message) {
//TODO: kA, etc
},
{
issuer: config.domain,
issuer: config.get('domain'),
issuedAt: new Date(now),
expiresAt: new Date(now + message.duration)
},
null,
config.idpSecretKey,
_privKey,
function (err, cert) {
process.send({ err: err, cert: cert});
}

Просмотреть файл

@ -0,0 +1,60 @@
#!/usr/bin/env node
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* scripts/gen_keys.js creates public and private keys suitable for
key signing Persona Primary IdP's.
Usage:
scripts/gen_keys.js
Will create these files
server/config/public-key.json
server/config/secret-key.json
If these files already exist, this script will show an error message
and exit. You must remove both keys if you want to generate a new
keypair.
*/
const jwcrypto = require("jwcrypto");
const fs = require('fs');
const assert = require("assert");
const configDir = fs.realpathSync(__dirname + "/../config");
const pubKeyFile = configDir + "/public-key.json";
const secretKeyFile = configDir + "/secret-key.json";
require("jwcrypto/lib/algs/rs");
try {
assert(fs.existsSync(configDir), "Config dir" + configDir + " not found");
assert(! fs.existsSync(pubKeyFile), "public key file: ["+pubKeyFile+"] already exists");
assert(! fs.existsSync(secretKeyFile), "public key file: ["+secretKeyFile+"] already exists");
} catch(e) {
console.error("Error: " + e.message);
process.exit(1);
}
console.log("Generating keypair. (install libgmp if this takes more than a second)");
// wondering about `keysize: 256`?
// well, 257 = 2048bit key
// still confused? see: https://github.com/mozilla/jwcrypto/blob/master/lib/algs/ds.js#L37-L57
jwcrypto.generateKeypair(
{ algorithm: 'RS', keysize: 256 },
function(err, keypair) {
var pubKey = keypair.publicKey.serialize();
var secretKey = keypair.secretKey.serialize();
fs.writeFileSync(pubKeyFile, pubKey);
console.log("Public Key saved:", pubKeyFile);
fs.writeFileSync(secretKeyFile, secretKey);
console.log("Secret Key saved:", pubKeyFile);
}
);

Просмотреть файл

@ -1,6 +1,10 @@
var assert = require('assert');
//var config = require('../../lib/config');
var helpers = require('../helpers');
var jwcrypto = require('jwcrypto');
// algorithms
require("jwcrypto/lib/algs/rs");
var testClient = new helpers.TestClient();
@ -9,7 +13,7 @@ var TEST_PASSWORD = 'foo';
var TEST_KB = 'secret!';
describe('user', function() {
var sessionId;
var sessionId, accountToken, pubkey, signToken;
it('should create a new account', function(done) {
testClient.makeRequest('POST', '/create', {
@ -121,6 +125,8 @@ describe('user', function() {
}
}, function(res) {
try {
accountToken = res.result.accountToken;
assert.ok(res.result.accountToken);
assert.ok(res.result.kA);
assert.equal(res.result.kB, TEST_KB);
@ -148,5 +154,48 @@ describe('user', function() {
});
});
it('should get signToken', function(done) {
testClient.makeRequest('POST', '/signToken', {
payload: {
accountToken: accountToken
}
}, function(res) {
try {
assert.equal(res.statusCode, 200);
signToken = res.result.signToken;
assert.ok(res.result.signToken);
} catch (e) {
return done(e);
}
done();
});
});
it('should generate a pubkey', function(done) {
jwcrypto.generateKeypair({ algorithm: "RS", keysize: 64 }, function(err, keypair) {
pubkey = keypair.publicKey;
done(err);
});
});
it('should sign a pubkey', function(done) {
testClient.makeRequest('POST', '/sign', {
payload: {
token: signToken,
publicKey: pubkey.serialize(),
duration: 50000
}
}, function(res) {
try {
assert.equal(res.statusCode, 200);
// check for rough format of a cert
assert.equal(res.result.split(".").length, 3);
} catch (e) {
return done(e);
}
done();
});
});
});