зеркало из https://github.com/mozilla/fxa.git
now generates certs from a signToken and public key
This commit is contained in:
Родитель
c0ffe969f8
Коммит
4990878d19
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче