├── .travis.yml ├── .gitignore ├── pkitemplate ├── openssl_apicert.cnf.tpl ├── openssl_ocsp.cnf.tpl ├── openssl_root.cnf.tpl └── openssl_intermediate.cnf.tpl ├── cert_fingerprint.js ├── package.json ├── crl.js ├── LICENSE ├── validator.js ├── api.js ├── nodepkictl.js ├── publicDl.js ├── api ├── ca.js ├── auth.js └── certificate.js ├── ocsp-server.js ├── README.md ├── certdb.js ├── config.default.yml ├── auth.js ├── API.md ├── server.js └── genpki.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "4.6" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## 2 | ## Ignore installed NodeJS modules 3 | ## 4 | 5 | node_modules/ 6 | 7 | 8 | ## 9 | ## Ignore dynamic directories 10 | ## 11 | 12 | data/ 13 | tmp/ 14 | -------------------------------------------------------------------------------- /pkitemplate/openssl_apicert.cnf.tpl: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 2048 3 | distinguished_name = req_distinguished_name 4 | string_mask = utf8only 5 | 6 | # SHA-1 is deprecated, so use SHA-2 instead. 7 | default_md = sha256 8 | 9 | prompt = no 10 | 11 | 12 | [ req_distinguished_name ] 13 | C={country} 14 | ST={state} 15 | L={locality} 16 | O={organization} 17 | CN={commonname} 18 | -------------------------------------------------------------------------------- /pkitemplate/openssl_ocsp.cnf.tpl: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 2048 3 | distinguished_name = req_distinguished_name 4 | string_mask = utf8only 5 | 6 | # SHA-1 is deprecated, so use SHA-2 instead. 7 | default_md = sha256 8 | 9 | prompt = no 10 | 11 | 12 | [ req_distinguished_name ] 13 | C={country} 14 | ST={state} 15 | L={locality} 16 | O={organization} 17 | CN={commonname} 18 | -------------------------------------------------------------------------------- /cert_fingerprint.js: -------------------------------------------------------------------------------- 1 | var log = require('fancy-log'); 2 | var exec = require('child_process').exec; 3 | var fs = require('fs-extra'); 4 | 5 | var getFingerprint = function(certfile) { 6 | return new Promise(function(resolve, reject) { 7 | exec('openssl x509 -noout -in ' + certfile + ' -fingerprint -sha256', { 8 | cwd: global.paths.basepath 9 | }, function(error, stdout, stderr) { 10 | var filter = /=([A-F0-9\:]*)/; 11 | var matches = filter.exec(stdout) 12 | var fingerprint = matches[1]; 13 | resolve(fingerprint); 14 | }); 15 | }); 16 | }; 17 | 18 | module.exports = { 19 | getFingerprint: getFingerprint 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodepki", 3 | "version": "0.2.0", 4 | "description": "NodeJS-based PKI server for x509 certificate management.", 5 | "author": "Thomas Leister ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "x509", 9 | "pki", 10 | "ca" 11 | ], 12 | "main": "server.js", 13 | "scripts": { 14 | "test": "echo \"There is no test.\" && exit 0;" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/ThomasLeister/nodepki.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/ThomasLeister/nodepki/issues" 22 | }, 23 | "dependencies": { 24 | "express": "4.x", 25 | "js-yaml": "3.x", 26 | "body-parser": "1.x", 27 | "uuid": "3.x", 28 | "fancy-log": "1.x", 29 | "ajv": "4.x", 30 | "fs-extra": "2.0.x", 31 | "figlet": "1.2.x", 32 | "command-exists": "1.2.x", 33 | "yargs": "6.6.x" 34 | }, 35 | "homepage": "https://github.com/ThomasLeister/nodepki#readme", 36 | "devDependencies": {} 37 | } 38 | -------------------------------------------------------------------------------- /crl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Creates CRL 3 | */ 4 | 5 | 6 | var spawn = require('child_process').spawn; 7 | var log = require('fancy-log'); 8 | var fs = require('fs-extra'); 9 | 10 | /* 11 | * Creates / updates CRL and overwrites old version. 12 | */ 13 | var createCRL = function() { 14 | crl = spawn('openssl', [ 15 | 'ca', 16 | '-config', 'openssl.cnf', 17 | '-gencrl', 18 | '-out', 'crl/crl.pem' 19 | ], { 20 | cwd: global.paths.pkipath + 'intermediate', 21 | shell: true, 22 | detached: true 23 | }); 24 | 25 | // Enter ocsp private key password 26 | crl.stdin.write(global.config.ca.intermediate.passphrase + '\n'); 27 | 28 | crl.on('error', function(error) { 29 | log("Error during crl generation: " + error); 30 | }); 31 | 32 | crl.on('exit', function(code, signal){ 33 | if(code === 0) { 34 | log("CRL successfully created"); 35 | } else { 36 | log.error("Error during CRL creation") 37 | } 38 | }); 39 | }; 40 | 41 | 42 | module.exports = { 43 | createCRL: createCRL 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ADITO Software GmbH / Thomas Leister 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /validator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Validator for API inputs 3 | * Utilizes AJV 4 | */ 5 | 6 | var log = require('fancy-log'); 7 | 8 | var Ajv = require('ajv'); 9 | var ajv = Ajv({allErrors: true}); 10 | 11 | 12 | var validator = {}; 13 | 14 | validator.checkAPI = function(schema, data) { 15 | var valid = ajv.validate(schema, data); 16 | 17 | if(valid) { 18 | return { success: true }; 19 | } else { 20 | var errors = []; 21 | 22 | ajv.errors.forEach(function(error) { 23 | var message = ''; 24 | 25 | switch(error.keyword) { 26 | case 'required': 27 | // requirement not fulfilled. 28 | message = 'Property \'' + error.params.missingProperty + '\' is missing.'; 29 | break; 30 | case 'type': 31 | message = 'Wrong type: ' + error.dataPath + ' ' + error.message; 32 | break; 33 | default: 34 | message = 'Unknown input error. :('; 35 | } 36 | 37 | var pusherror = { 38 | message: message 39 | } 40 | 41 | errors.push(pusherror); 42 | }); 43 | 44 | return { success: false, errors: errors }; 45 | } 46 | }; 47 | 48 | 49 | module.exports = validator; 50 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * API registry for all HTTP API actions such as 3 | * GET, PUT, DELETE 4 | */ 5 | 6 | // 3rd party modules 7 | var fs = require('fs') 8 | var express = require('express') 9 | var bodyparser = require('body-parser') 10 | 11 | // Custom modules 12 | var certapi = require('./api/certificate.js') 13 | var caapi = require('./api/ca.js') 14 | var authapi = require('./api/auth.js') 15 | 16 | 17 | var apipath = '/api/v1'; 18 | 19 | /** 20 | * Initializes API paths. 21 | */ 22 | var initAPI = function(app) { 23 | app.post(apipath + '/certificate/request/', function(req, res) { 24 | certapi.certificate.request(req, res); 25 | }); 26 | 27 | app.post(apipath + '/certificate/revoke/', function(req, res) { 28 | certapi.certificate.revoke(req, res); 29 | }); 30 | 31 | app.post(apipath + '/ca/cert/get/', function(req, res) { 32 | caapi.cert.get(req, res); 33 | }); 34 | 35 | app.post(apipath + '/certificates/list/', function(req, res) { 36 | certapi.certificates.list(req, res); 37 | }); 38 | 39 | app.post(apipath + '/certificate/get/', function(req, res) { 40 | certapi.certificate.get(req, res); 41 | }); 42 | 43 | app.post(apipath + '/auth/check/', function(req, res) { 44 | authapi.auth.check(req, res); 45 | }); 46 | }; 47 | 48 | 49 | 50 | // Export initAPI() function (called by server.js) 51 | module.exports = { 52 | initAPI: initAPI 53 | } 54 | -------------------------------------------------------------------------------- /nodepkictl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NodePKI management tool 3 | */ 4 | 5 | var log = require('fancy-log'); 6 | var yargs = require('yargs'); 7 | 8 | var auth = require('./auth.js'); 9 | 10 | 11 | /** 12 | * Register subcommands 13 | */ 14 | var argv = yargs 15 | .usage("Usage: $0 [options]") 16 | .command("useradd", "Create a new API user", function(yargs){ 17 | var argv = yargs 18 | .option('username', { 19 | demand: true, 20 | describe: "Username for new user", 21 | type: "string" 22 | }) 23 | .option('password', { 24 | demand: true, 25 | describe: "Password for new user", 26 | type: "string" 27 | }) 28 | .example("$0 useradd --username thomas --password thomaspassword") 29 | .argv; 30 | 31 | if(auth.addUser(argv.username, argv.password)) { 32 | log("User created successfully."); 33 | } else { 34 | log("Error: Username already exists!"); 35 | } 36 | }) 37 | .command("userdel", "Delete existing API user", function(yargs){ 38 | var argv = yargs 39 | .option('username', { 40 | demand: true, 41 | describe: "Username for new user", 42 | type: "string" 43 | }) 44 | .example("$0 userdel --username thomas") 45 | .argv; 46 | 47 | if(auth.delUser(argv.username)) { 48 | log("User deleted successfully."); 49 | } else { 50 | log("Error: Username does not exist!"); 51 | } 52 | }) 53 | .demandCommand(1) 54 | .help("h") 55 | .alias("h", "help") 56 | .argv 57 | -------------------------------------------------------------------------------- /publicDl.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra') 2 | 3 | var publicpath = '/public' 4 | 5 | /** 6 | * Initializes API paths. 7 | */ 8 | var initPublicDl = function(app) { 9 | app.get(publicpath + '/ca/root/cert', function(req, res) { 10 | var certificate = fs.readFileSync(global.paths.pkipath + 'root/root.cert.pem') 11 | var filename = global.config.ca.root.commonname.replace(/\ /g, '_').toLowerCase() 12 | 13 | res.setHeader('Content-disposition', 'attachment; filename=' + filename + '.cert.pem'); 14 | res.setHeader('Content-type', 'application/x-pem-file') 15 | res.end(certificate) 16 | }); 17 | 18 | app.get(publicpath + '/ca/intermediate/cert', function(req, res) { 19 | var certificate = fs.readFileSync(global.paths.pkipath + 'intermediate/intermediate.cert.pem') 20 | var filename = global.config.ca.intermediate.commonname.replace(/\ /g, '_').toLowerCase() 21 | 22 | res.setHeader('Content-disposition', 'attachment; filename=' + filename + '.cert.pem'); 23 | res.setHeader('Content-type', 'application/x-pem-file') 24 | res.end(certificate) 25 | }); 26 | 27 | app.get(publicpath + '/ca/intermediate/crl', function(req, res) { 28 | var certificate = fs.readFileSync(global.paths.pkipath + 'intermediate/crl/crl.pem') 29 | var filename = global.config.ca.intermediate.commonname.replace(/\ /g, '_').toLowerCase() 30 | 31 | console.log(filename) 32 | 33 | res.setHeader('Content-disposition', 'attachment; filename=' + filename + '.crl.pem'); 34 | res.setHeader('Content-type', 'application/x-pem-file') 35 | res.end(certificate) 36 | }); 37 | }; 38 | 39 | 40 | 41 | // Export initAPI() function (called by server.js) 42 | module.exports = { 43 | initPublicDl: initPublicDl 44 | } 45 | -------------------------------------------------------------------------------- /api/ca.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CA API 3 | */ 4 | 5 | var log = require('fancy-log'); 6 | var fs = require('fs-extra'); 7 | 8 | var ca = {}; 9 | ca.cert = {}; 10 | 11 | 12 | 13 | /* 14 | * Response helper function for nicer code :) 15 | */ 16 | function respond(res, resobj) { 17 | resobj.end(JSON.stringify(res)) 18 | } 19 | 20 | 21 | 22 | /** 23 | * Get CA Cert 24 | */ 25 | ca.cert.get = function(req, res) { 26 | var data = req.body.data; 27 | 28 | log("Client is requesting certificate of CA " + data.ca); 29 | 30 | new Promise(function(resolve, reject) { 31 | if(data.chain) { 32 | log("Client is requesting chain version") 33 | } 34 | 35 | switch(data.ca) { 36 | case 'root': 37 | var cert = fs.readFileSync(global.paths.pkipath + 'root/root.cert.pem', 'utf8'); 38 | resolve(cert); 39 | break; 40 | case 'intermediate': 41 | if(data.chain) { 42 | var cert = fs.readFileSync(global.paths.pkipath + 'intermediate/ca-chain.cert.pem', 'utf8'); 43 | } else { 44 | var cert = fs.readFileSync(global.paths.pkipath + 'intermediate/intermediate.cert.pem', 'utf8'); 45 | } 46 | resolve(cert); 47 | break; 48 | default: 49 | reject("Invalid CA") 50 | } 51 | }) 52 | .then(function(cert) { 53 | respond({ 54 | success: true, 55 | cert: cert 56 | }, res); 57 | }) 58 | .catch(function(error) { 59 | respond({ 60 | success: false, 61 | errors: [ 62 | { code: 101, message: error } 63 | ] 64 | }, res); 65 | }); 66 | }; 67 | 68 | module.exports = ca; 69 | -------------------------------------------------------------------------------- /ocsp-server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OCSP-Server via OpenSSL 3 | */ 4 | 5 | var spawn = require('child_process').spawn; 6 | var log = require('fancy-log'); 7 | 8 | var ocsp; 9 | 10 | /** 11 | * Function starts OpenSSL server 12 | */ 13 | var startServer = function() { 14 | return new Promise(function(resolve, reject) { 15 | log("Starting OCSP server ...") 16 | 17 | ocsp = spawn('openssl', [ 18 | 'ocsp', 19 | '-port', global.config.server.ip+':'+global.config.server.ocsp.port, 20 | '-text', 21 | '-sha256', 22 | '-index', 'index.txt', 23 | '-CA', 'ca-chain.cert.pem', 24 | '-rkey', 'ocsp/ocsp.key.pem', 25 | '-rsigner', 'ocsp/ocsp.cert.pem' 26 | ], { 27 | cwd: global.paths.pkipath + 'intermediate', 28 | detached: true, 29 | shell: true 30 | }); 31 | 32 | // Enter ocsp private key password 33 | ocsp.stdin.write(global.config.ca.intermediate.ocsp.passphrase + '\n'); 34 | 35 | log(">>>>>> OCSP server is listening on " + global.config.server.ip + ':' + global.config.server.ocsp.port + " <<<<<<"); 36 | 37 | resolve(); 38 | 39 | ocsp.on('error', function(error) { 40 | log("OCSP server startup error: " + error); 41 | }); 42 | 43 | ocsp.on('close', function(code){ 44 | if(code === null) { 45 | log("OCSP server exited successfully."); 46 | } else { 47 | log.error("OCSP exited with code " + code); 48 | } 49 | }); 50 | }); 51 | }; 52 | 53 | 54 | var stopServer = function() { 55 | ocsp.kill('SIGHUP'); 56 | log("OCSP server stopped."); 57 | }; 58 | 59 | 60 | module.exports = { 61 | startServer: startServer, 62 | stopServer: stopServer 63 | } 64 | -------------------------------------------------------------------------------- /api/auth.js: -------------------------------------------------------------------------------- 1 | var log = require('fancy-log'); 2 | var validator = require('../validator.js'); 3 | var authMod = require('../auth.js'); 4 | 5 | var auth = {}; 6 | 7 | 8 | /* 9 | * Response helper function for nicer code :) 10 | */ 11 | function respond(res, resobj) { 12 | resobj.end(JSON.stringify(res)) 13 | } 14 | 15 | 16 | function wrongAPISchema(apierrors, res) { 17 | var errors = [] 18 | 19 | apierrors.forEach(function(apierror) { 20 | errors.push({ code: 100, message: apierror.message }); 21 | }); 22 | 23 | var resobj = { 24 | success: false, 25 | errors: errors 26 | } 27 | 28 | res.end(JSON.stringify(resobj)) 29 | }; 30 | 31 | 32 | auth.check = function(req, res) { 33 | // Validate user input 34 | var schema = { 35 | "properties": { 36 | "auth": { 37 | "type": "object", 38 | "properties": { 39 | "username": { "type": "string"}, 40 | "password": { "type": "string"} 41 | }, 42 | "required": [ "username", "password" ] 43 | } 44 | }, 45 | "required": [ "auth" ] 46 | } 47 | 48 | // Check API conformity 49 | var check = validator.checkAPI(schema, req.body) 50 | if(check.success === false) { 51 | wrongAPISchema(check.errors, res); 52 | return; 53 | } 54 | 55 | var auth = req.body.auth; 56 | 57 | // Check access 58 | if (authMod.checkUser(auth.username, auth.password) === false) { 59 | respond({ 60 | success: true, 61 | data: { valid: false } 62 | }, res); 63 | } else { 64 | respond({ 65 | success: true, 66 | data: { valid: true } 67 | }, res); 68 | } 69 | } 70 | 71 | 72 | module.exports = { 73 | auth: auth 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodePKI 2 | 3 | *NodePKI is a simple NodeJS based PKI manager for small corporate environments.* 4 | 5 | --- 6 | 7 | 8 | ## Implemented Features 9 | 10 | * Auto-create a PKI with root CA and intermediate CA 11 | * Request new certificates 12 | * List available certificates 13 | * Download issued certificate files 14 | * Revoke issued certificate 15 | * OSCP server 16 | * CRL HTTP server 17 | 18 | 19 | ## Requirements 20 | 21 | * Linux OS 22 | * NodeJS 23 | * NPM 24 | * OpenSSL 25 | 26 | ## Run NodePKI with Docker 27 | 28 | **The recommended way to run NodePKI is to make use of the NodePKI Docker image.** Find more information here: [NodePKI Docker image](https://github.com/aditosoftware/nodepki-docker/) 29 | 30 | 31 | ## Setup instructions 32 | 33 | git clone https://github.com/aditosoftware/nodepki.git 34 | cd nodepki 35 | npm install 36 | 37 | 38 | ### Configure NodePKI 39 | 40 | There is an example config file "config.yml.default" which can be copied to "config.yml". Change config.yml to fit your environment. The passwords defined in config.yml will be used to create the PKI. 41 | 42 | 43 | ## Start all the things! 44 | 45 | Start your API server: 46 | 47 | nodejs server.js 48 | 49 | CA files in data/mypki will be created on first startup. 50 | 51 | 52 | ## API user login 53 | 54 | ### Add new user 55 | 56 | nodejs nodepkictl useradd --username user1 --password user1password 57 | 58 | ### Remove user 59 | 60 | nodejs nodepkictl userdel --username user1 61 | 62 | 63 | ## API usage 64 | 65 | For information on how to use the API, read [API.md](/API.md) 66 | 67 | 68 | ## Using the server via client 69 | 70 | Use [nodepki-client](https://github.com/aditosoftware/nodepki-client/) to request certificates and manage your PKI. If you prefer using a GUI, consider using [nodepki-webclient](https://github.com/aditosoftware/nodepki-webclient/). 71 | -------------------------------------------------------------------------------- /certdb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Poor man's in-memory DB for quick certificate queries 3 | */ 4 | 5 | 6 | var log = require('fancy-log'); 7 | var crl = require('./crl.js'); 8 | 9 | certificates = new Array(); 10 | 11 | // Sample: V 270129084423Z 270129084423Z 100E unknown /C=DE/ST=Germany/O=ADITO Software GmbH/OU=IT/CN=ADITO General Intermediate CA/emailAddress=it@adito.de 12 | var regex = /([R,E,V])(\t)(.*)(\t)(.*)(\t)([\dA-F]*)(\t)(unknown)(\t)(.*)/; 13 | 14 | 15 | /* 16 | * Re-indexes OpenSSL index.txt file and stores datasets in array 'certificates' 17 | */ 18 | var reindex = function() { 19 | return new Promise(function(resolve, reject) { 20 | log.info("Reindexing CertDB ..."); 21 | 22 | // Index-Datei öffnen 23 | var lineReader = require('readline').createInterface({ 24 | input: require('fs').createReadStream(global.paths.pkipath + 'intermediate/index.txt') 25 | }); 26 | 27 | certificates = []; 28 | 29 | lineReader.on('line', function (line) { 30 | // Regex auf diese Zeile anwenden, um einzelne Spalten zu gewinnen. 31 | var columns = regex.exec(line); 32 | 33 | if(columns !== null){ 34 | var certificate = { 35 | state: columns[1], 36 | expirationtime: columns[3], 37 | revocationtime: columns[5], 38 | serial: columns[7], 39 | subject: columns[11] 40 | }; 41 | 42 | certificates.push(certificate); 43 | } else { 44 | log.error("Error while parsing index.txt line :("); 45 | } 46 | }); 47 | 48 | lineReader.on('close', function() { 49 | log.info("Reindexing finished"); 50 | 51 | // Re-Create CRL 52 | crl.createCRL(); 53 | 54 | resolve(); 55 | }); 56 | }); 57 | } 58 | 59 | 60 | /* 61 | * Return all certificates 62 | */ 63 | var getIndex = function() { 64 | return certificates; 65 | } 66 | 67 | 68 | module.exports = { 69 | reindex: reindex, 70 | getIndex: getIndex 71 | } 72 | -------------------------------------------------------------------------------- /config.default.yml: -------------------------------------------------------------------------------- 1 | ### 2 | ### Server config: IP-Address and port to listen to. 3 | ### 4 | 5 | server: 6 | # E.g.: 0.0.0.0 7 | ip: CA_API_SERVER_BIND_IP_ADDRESS 8 | http: 9 | # E.g.: ca.adito.local 10 | domain: CA_API_SERVER_URL 11 | # E.g.: 8080 12 | port: CA_API_SERVER_PLAIN_PORT 13 | ocsp: 14 | # E.g.: ca.adito.local 15 | domain: CA_OSCP_SERVER_URL 16 | # E.g.: 2560 17 | port: CA_OSCP_SERVER_PORT 18 | 19 | 20 | 21 | ### 22 | ### CA config: Passphrase for CA Key 23 | ### 24 | 25 | ca: 26 | root: 27 | # E.g.: uDaMhCfFVcPJxZgkctKxKE2vYrwYHEnhcp 28 | passphrase: ROOT_PASSPHRASE 29 | # E.g.: 3650 30 | days: CA_CERT_EXPIRE_IN_DAYS 31 | # E.g.: DE 32 | country: COUNTRY_CODE 33 | # E.g.: Bavaria 34 | state: STATE_NAME 35 | # E.g.: Geisenhausen 36 | locality: LOCALITY_NAME 37 | # E.g.: ADITO Software GmbH 38 | organization: ORGANIZATION_NAME 39 | # E.g.: Root CA ADITO 40 | commonname: ROOT_CA_COMMON_NAME 41 | intermediate: 42 | # E.g.: 4vhsDBWtnTXuUsQEBTSxZRKvAj2dKcn 43 | passphrase: INTERMEDIATE_PASSPHRASE 44 | # E.g.: 3650 45 | days: CA_CERT_EXPIRE_IN_DAYS 46 | # E.g.: DE 47 | country: COUNTRY_CODE 48 | # E.g.: Bavaria 49 | state: STATE_NAME 50 | # E.g.: Geisenhausen 51 | locality: LOCALITY_NAME 52 | # E.g.: ADITO Software GmbH 53 | organization: ORGANIZATION_NAME 54 | # E.g.: Intermediate CA ADITO 55 | commonname: INTERMEDIATE_CA_COMMON_NAME 56 | ocsp: 57 | # E.g.: gpCnCFZuraQYtQaQNWs4apWK2W 58 | passphrase: OCSP_PASSPHRASE 59 | # E.g.: DE 60 | country: COUNTRY_CODE 61 | # E.g.: http://ca.adito.local:2560 62 | url: CA_OSCP_SERVER_HTTP_URL 63 | crl: 64 | # E.g.: http://ca.adito.local:8080/public/ca/intermediate/crl 65 | url: CA_CRL_SERVER_HTTP_URL 66 | 67 | 68 | ### 69 | ### Settings for end user certificates 70 | ### 71 | cert: 72 | # E.g.: 1 73 | lifetime_default: CERT_MIN_LIFETIME_IN_DAYS 74 | # E.g.: 365 75 | lifetime_max: CERT_MAX_LIFETIME_IN_DAYS 76 | -------------------------------------------------------------------------------- /auth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Auth module 3 | */ 4 | 5 | var crypto = require('crypto'); 6 | var fs = require('fs-extra'); 7 | 8 | 9 | /* 10 | * Checks login credentials 11 | * Input: Username, password (plain) 12 | */ 13 | var checkUser = function(username, password) { 14 | var hash = crypto.createHash('sha256').update(password).digest('base64'); 15 | 16 | // Check if there is an entry with username:hash 17 | // ... 18 | var expected = username + ':' + hash; 19 | 20 | // Read password file 21 | var passfile = fs.readFileSync('data/user.db', 'utf8'); 22 | var lines = passfile.split('\n'); 23 | 24 | var found = false; 25 | lines.forEach(function(line) { 26 | if (line === expected) found = true; 27 | }); 28 | 29 | return found; 30 | }; 31 | 32 | 33 | 34 | /* 35 | * Add a new user to DB 36 | */ 37 | var addUser = function(username, password) { 38 | // Make sure DB file exists ... 39 | fs.ensureFileSync('data/user.db'); 40 | 41 | // Calc passhash 42 | var passhash = crypto.createHash('sha256').update(password).digest('base64'); 43 | 44 | // Read existing file 45 | var passfile = fs.readFileSync('data/user.db', 'utf8'); 46 | 47 | // Check if user alreadys exists 48 | var lines = passfile.split('\n'); 49 | var found = false; 50 | lines.forEach(function(line) { 51 | var line_username = line.split(':')[0]; 52 | if (line_username === username) found = true; 53 | }); 54 | 55 | if(found === false) { 56 | // Update file 57 | passfile = passfile + username + ':' + passhash + '\n'; 58 | fs.writeFileSync('data/user.db', passfile, 'utf8'); 59 | 60 | return true; 61 | } else { 62 | return false; 63 | } 64 | }; 65 | 66 | 67 | 68 | /* 69 | * Delete user from DB 70 | */ 71 | var delUser = function(username) { 72 | fs.ensureFileSync('data/user.db'); 73 | 74 | var passfile = fs.readFileSync('data/user.db', 'utf8'); 75 | var lines = passfile.split('\n'); 76 | var changed = false; 77 | 78 | var passfile_out = ''; 79 | 80 | // Re-write file without user 81 | 82 | lines.forEach(function(line) { 83 | if(line !== '') { 84 | var line_username = line.split(':')[0]; 85 | 86 | if(line_username !== username) { 87 | passfile_out += line + '\n' 88 | } else { 89 | changed = true; 90 | } 91 | } 92 | }); 93 | 94 | fs.writeFileSync('data/user.db', passfile_out); 95 | 96 | return changed; 97 | }; 98 | 99 | 100 | module.exports = { 101 | addUser: addUser, 102 | checkUser: checkUser, 103 | delUser: delUser 104 | } 105 | -------------------------------------------------------------------------------- /pkitemplate/openssl_root.cnf.tpl: -------------------------------------------------------------------------------- 1 | [ca] 2 | 3 | default_ca = CA_default 4 | 5 | [ CA_default ] 6 | # Directory and file locations. 7 | dir = {basedir} 8 | certs = $dir/certs 9 | crl_dir = $dir/crl 10 | new_certs_dir = $certs 11 | database = $dir/index.txt 12 | serial = $dir/serial 13 | RANDFILE = $dir/.rand 14 | 15 | # The root key and root certificate. 16 | private_key = $dir/root.key.pem 17 | certificate = $dir/root.cert.pem 18 | 19 | # For certificate revocation lists. 20 | crlnumber = $dir/crlnumber 21 | crl = $dir/crl/root.crl.pem 22 | crl_extensions = crl_ext 23 | default_crl_days = 7 24 | 25 | # SHA-1 is deprecated, so use SHA-2 instead. 26 | default_md = sha256 27 | 28 | name_opt = ca_default 29 | cert_opt = ca_default 30 | default_days = {days} 31 | preserve = no 32 | policy = policy_strict 33 | 34 | 35 | [ policy_strict ] 36 | countryName = match 37 | stateOrProvinceName = match 38 | localityName = optional 39 | organizationName = match 40 | organizationalUnitName = optional 41 | commonName = supplied 42 | emailAddress = optional 43 | 44 | 45 | 46 | [ req ] 47 | default_bits = 2048 48 | distinguished_name = req_distinguished_name 49 | string_mask = utf8only 50 | default_md = sha256 51 | 52 | # Extension to add when the -x509 option is used. 53 | x509_extensions = v3_ca 54 | 55 | prompt = no 56 | 57 | 58 | [ req_distinguished_name ] 59 | C={country} 60 | ST={state} 61 | L={locality} 62 | O={organization} 63 | CN={commonname} 64 | 65 | 66 | ### 67 | ### Extensions 68 | ### 69 | 70 | ### For CA 71 | [ v3_ca ] 72 | subjectKeyIdentifier = hash 73 | authorityKeyIdentifier = keyid:always,issuer 74 | basicConstraints = critical, CA:true 75 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 76 | 77 | ### For Intermediate CA 78 | [ v3_intermediate_ca ] 79 | subjectKeyIdentifier = hash 80 | authorityKeyIdentifier = keyid:always,issuer 81 | basicConstraints = critical, CA:true, pathlen:0 82 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 83 | 84 | 85 | ### For server certificates (API Cert) 86 | [ server_cert ] 87 | basicConstraints = CA:FALSE 88 | nsCertType = server 89 | nsComment = "OpenSSL Generated Server Certificate" 90 | subjectKeyIdentifier = hash 91 | authorityKeyIdentifier = keyid,issuer:always 92 | keyUsage = critical, digitalSignature, keyEncipherment 93 | extendedKeyUsage = serverAuth 94 | 95 | 96 | ### For CRLs 97 | [ crl_ext ] 98 | authorityKeyIdentifier=keyid:always 99 | 100 | 101 | ### For OCSP certificates 102 | [ ocsp ] 103 | basicConstraints = CA:FALSE 104 | subjectKeyIdentifier = hash 105 | authorityKeyIdentifier = keyid,issuer 106 | keyUsage = critical, digitalSignature 107 | extendedKeyUsage = critical, OCSPSigning 108 | -------------------------------------------------------------------------------- /pkitemplate/openssl_intermediate.cnf.tpl: -------------------------------------------------------------------------------- 1 | [ca] 2 | 3 | default_ca = CA_default 4 | 5 | [ CA_default ] 6 | # Directory and file locations. 7 | dir = {basedir} 8 | certs = $dir/certs 9 | crl_dir = $dir/crl 10 | new_certs_dir = $certs 11 | database = $dir/index.txt 12 | serial = $dir/serial 13 | RANDFILE = $dir/.rand 14 | 15 | # The root key and root certificate. 16 | private_key = $dir/intermediate.key.pem 17 | certificate = $dir/intermediate.cert.pem 18 | 19 | # For certificate revocation lists. 20 | crlnumber = $dir/crlnumber 21 | crl = $dir/crl/intermediate.crl.pem 22 | crl_extensions = crl_ext 23 | default_crl_days = 7 24 | 25 | # SHA-1 is deprecated, so use SHA-2 instead. 26 | default_md = sha256 27 | 28 | name_opt = ca_default 29 | cert_opt = ca_default 30 | default_days = {days} 31 | preserve = no 32 | policy = policy_loose 33 | unique_subject = no 34 | 35 | # Copy SAN 36 | copy_extensions = copy 37 | 38 | 39 | [ policy_loose ] 40 | countryName = optional 41 | stateOrProvinceName = optional 42 | localityName = optional 43 | organizationName = optional 44 | organizationalUnitName = optional 45 | commonName = supplied 46 | emailAddress = optional 47 | 48 | 49 | [ req ] 50 | default_bits = 2048 51 | distinguished_name = req_distinguished_name 52 | string_mask = utf8only 53 | 54 | # SHA-1 is deprecated, so use SHA-2 instead. 55 | default_md = sha256 56 | 57 | # Extension to add when the -x509 option is used. 58 | x509_extensions = v3_ca 59 | 60 | prompt = no 61 | 62 | [ req_distinguished_name ] 63 | C={country} 64 | ST={state} 65 | L={locality} 66 | O={organization} 67 | CN={commonname} 68 | 69 | 70 | 71 | ### 72 | ### Extensions 73 | ### 74 | 75 | [ v3_ca ] 76 | subjectKeyIdentifier = hash 77 | authorityKeyIdentifier = keyid:always,issuer 78 | basicConstraints = critical, CA:true 79 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 80 | 81 | 82 | ### For User (Client) certificates 83 | [ usr_cert ] 84 | basicConstraints = CA:FALSE 85 | nsCertType = client, email 86 | nsComment = "OpenSSL Generated Client Certificate" 87 | subjectKeyIdentifier = hash 88 | authorityKeyIdentifier = keyid,issuer 89 | keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment 90 | extendedKeyUsage = clientAuth, emailProtection 91 | 92 | 93 | ### For server certificates 94 | [ server_cert ] 95 | basicConstraints = CA:FALSE 96 | nsCertType = server 97 | nsComment = "OpenSSL Generated Server Certificate" 98 | subjectKeyIdentifier = hash 99 | authorityKeyIdentifier = keyid,issuer:always 100 | keyUsage = critical, digitalSignature, keyEncipherment 101 | extendedKeyUsage = serverAuth 102 | issuerAltName = issuer:copy 103 | crlDistributionPoints = URI:{crlurl} 104 | authorityInfoAccess = OCSP;URI:{ocspurl} 105 | 106 | 107 | ### For CRLs 108 | [ crl_ext ] 109 | authorityKeyIdentifier=keyid:always 110 | 111 | 112 | ### For OCSP certificates 113 | [ ocsp ] 114 | basicConstraints = CA:FALSE 115 | subjectKeyIdentifier = hash 116 | authorityKeyIdentifier = keyid,issuer 117 | keyUsage = critical, digitalSignature 118 | extendedKeyUsage = critical, OCSPSigning 119 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | API Request bodies consist of a "data" object containing parameters for the requested operation and an "auth" object containing username and password of the user: 4 | 5 | { 6 | data: { 7 | ... 8 | }, 9 | auth: { 10 | username: "thomas", 11 | password: "test" 12 | } 13 | } 14 | 15 | For some API calls authentication is not required. 16 | 17 | API response bodies: 18 | 19 | { 20 | success: , 21 | 22 | } 23 | 24 | 25 | ## Examples 26 | 27 | For better unstanding the general API usage: Two examples with cURL (the "-d" argument contains the JSON-formatted request) 28 | 29 | List all issued certificates: 30 | 31 | ``` 32 | curl -H "Content-type: application/json" -d '{ "data": { "state":"all" }, "auth": { "username":"thomas", "password":"test" } }' http://localhost:8080/api/v1/certificates/list 33 | ``` 34 | 35 | 36 | Request certificate from CSR: 37 | 38 | ``` 39 | curl -H "Content-type: application/json" -d '{ "data": { "applicant":"Thomas", "csr":"---CERTIFICATE SIGNING REQUEST---", "lifetime":365, "type":"server" }, "auth": { "username":"thomas", "password":"test" } }' http://localhost:8080/api/v1/certificate/request 40 | ``` 41 | 42 | 43 | 44 | ## Certificates 45 | 46 | ### Request certificate 47 | 48 | POST /api/v1/certificate/request/ 49 | 50 | Request params: 51 | * applicant: | Applicant, who requests certificate (for future usage) 52 | * csr: | CSR data in PEM format 53 | * lifetime: (optional) | Lifetime of certificate in days 54 | * type: (optional) | Certificate type. Can be 'server', 'client'. Defaults to 'server' 55 | 56 | Response attributes: 57 | * cert: | certificate 58 | 59 | 60 | 61 | ### Revoke certificate 62 | 63 | POST /api/v1/certificate/revoke/ 64 | 65 | Request params: 66 | * cert: | Certificate to revoke in PEM format 67 | 68 | Response attributes: success 69 | 70 | 71 | ### Get certificate 72 | 73 | POST /api/v1/certificate/get/ 74 | 75 | Request params: 76 | * serialnumber: | Serial number of the certificate 77 | 78 | Response attributes: 79 | * cert: | Certificate 80 | 81 | 82 | 83 | ### List certificates 84 | 85 | POST /api/v1/certificates/list/ 86 | 87 | Request params: 88 | * state: | 'valid', 'expired', 'revoked', 'all' 89 | 90 | Response body: 91 | { 92 | success: , 93 | certs: [ 94 | { 95 | state: , 96 | expirationtime: