Skip to content

Commit

Permalink
ocsp: initial
Browse files Browse the repository at this point in the history
  • Loading branch information
indutny committed May 21, 2015
0 parents commit d680b45
Show file tree
Hide file tree
Showing 12 changed files with 538 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
node_modules/
npm-debug.log
26 changes: 26 additions & 0 deletions README.md
@@ -0,0 +1,26 @@
# OCSP

#### LICENSE

This software is licensed under the MIT License.

Copyright Fedor Indutny, 2015.

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the
following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
4 changes: 4 additions & 0 deletions lib/ocsp.js
@@ -0,0 +1,4 @@
exports.Cache = require('./ocsp/cache');
exports.request = require('./ocsp/request');
exports.check = require('./ocsp/check');
exports.utils = require('./ocsp/utils');
116 changes: 116 additions & 0 deletions lib/ocsp/cache.js
@@ -0,0 +1,116 @@
var ocsp = require('../ocsp');

var url = require('url');
var rfc2560 = require('asn1.js-rfc2560');
var Buffer = require('buffer').Buffer;

function OCSPCache(options) {
this.options = options || {};
this.cache = {};

// Override methods
if (this.options.probe)
this.probe = this.options.probe;
if (this.options.store)
this.store = this.options.store;
if (this.options.filter)
this.filter = this.options.filter;
};
module.exports = OCSPCache;

OCSPCache.prototype.filter = function filter(url, callback) {
callback(null);
};

OCSPCache.prototype.probe = function probe(id, callback) {
if (this.cache.hasOwnProperty(id))
callback(null, this.cache[id]);
else
callback(null, false);
};

OCSPCache.prototype.store = function store(id, response, maxTime, callback) {
if (this.cache.hasOwnProperty(id))
clearTimeout(this.cache[id].timer);
this.cache[id] = {
response: response,
timer: setTimeout(function() {
delete this.cache[id];
}, maxTime)
};

callback(null, null);
};

OCSPCache.prototype.request = function request(id, data, callback) {
var self = this;

function done(err, response) {
if (callback)
callback(err, response);
callback = null;
}

// Check that url isn't blacklisted
this.filter(data.url, function(err) {
if (err)
return done(err, null);

ocsp.utils.getResponse(data.url, data.ocsp, onResponse);
});

function onResponse(err, ocsp) {
if (err)
return done(err);

// Respond early
done(null, ocsp);

// Try parsing and caching response
self.getMaxStoreTime(ocsp, function(err, maxTime) {
if (err)
return;

self.store(id, ocsp, maxTime, function(err) {
// No-op
});
});
}
};

OCSPCache.prototype.getMaxStoreTime = function getMaxStoreTime(ocsp, callback) {
try {
var basic = ocsp.utils.parseResponse(ocsp);
} catch (e) {
return callback(e);
}

// Not enough responses
if (basic.tbsResponseData.responses.length === 0)
return callback(new Error('No OCSP responses'));

var responses = basic.tbsResponseData.responses;

// Every response should be positive
var good = responses.every(function(response) {
return response.certStatus.type === 'good';
});

// No good - no cache
if (!good)
return callback(new Error('Some OCSP responses are not good'));

// Find minimum nextUpdate time
var nextUpdate = 0;
for (var i = 0; i < responses.length; i++) {
var response = responses[i];
var responseNext = response.nextUpdate;
if (!responseNext)
continue;

if (nextUpdate === 0 || nextUpdate > responseNext)
nextUpdate = responseNext;
}

return callback(null, Math.max(0, nextUpdate - new Date));
};
76 changes: 76 additions & 0 deletions lib/ocsp/check.js
@@ -0,0 +1,76 @@
var ocsp = require('../ocsp');

var rfc3280 = require('asn1.js-rfc3280');
var rfc2560 = require('asn1.js-rfc2560');

module.exports = function check(cert, issuer, cb) {
var sync = true;
try {
var req = ocsp.request.generate(cert, issuer);
} catch (e) {
return done(e);
}

var exts = req.cert.tbsCertificate.extensions;
var extnID = rfc3280['id-pe-authorityInfoAccess'].join('.');

var infoAccess = exts.filter(function(ext) {
return ext.extnID.join('.') === extnID;
});

if (infoAccess.length === 0)
return done(new Error('AuthorityInfoAccess not found in extensions'));

var ocspMethod = rfc2560['id-pkix-ocsp'].join('.');

var ocspURI = null;
var found = infoAccess.some(function(raw) {
try {
var ext = rfc3280.AuthorityInfoAccessSyntax.decode(raw.extnValue, 'der');
} catch (e) {
return false;
}

return ext.some(function(ad) {
if (ad.accessMethod.join('.') !== ocspMethod)
return false;

var loc = ad.accessLocation;
if (loc.type !== 'uniformResourceIdentifier')
return false;

ocspURI = loc.value + '';

return true;
});
});

if (!found)
return done(new Error('id-pkix-ocsp not found in AuthorityInfoAccess'));

sync = false;
ocsp.utils.getResponse(ocspURI, req.data, function(err, raw) {
if (err)
return done(err);

try {
var res = ocsp.utils.parseResponse(raw);
} catch (e) {
return done(e);
}

console.log(res);
});

function done(err, data) {
if (sync) {
sync = false;
process.nextTick(function() {
cb(err, data);
});
return;
}

cb(err, data);
}
};
63 changes: 63 additions & 0 deletions lib/ocsp/request.js
@@ -0,0 +1,63 @@
var crypto = require('crypto');
var rfc2560 = require('asn1.js-rfc2560');
var rfc3280 = require('asn1.js-rfc3280');
var Buffer = require('buffer').Buffer;

function sha1(data) {
return crypto.createHash('sha1').update(data).digest();
}

function toDER(raw) {
var der = raw.toString().match(
/-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/);
if (der)
der = new Buffer(der[1].replace(/[\r\n]/g, ''), 'base64');
else if (typeof raw === 'string')
der = new Buffer(raw);
else
der = raw;
return der;
}

exports.generate = function generate(rawCert, rawIssuer) {
var cert = rfc3280.Certificate.decode(toDER(rawCert), 'der');
var issuer = rfc3280.Certificate.decode(toDER(rawIssuer), 'der');
var tbsCert = cert.tbsCertificate;
var tbsIssuer = issuer.tbsCertificate;

var certID = {
hashAlgorithm: {
// algorithm: [ 2, 16, 840, 1, 101, 3, 4, 2, 1 ] // sha256
algorithm: [ 1, 3, 14, 3, 2, 26 ] // sha1
},
issuerNameHash: sha1(rfc3280.Name.encode(tbsCert.issuer, 'der')),
issuerKeyHash: sha1(
tbsIssuer.subjectPublicKeyInfo.subjectPublicKey.data),
serialNumber: tbsCert.serialNumber
};

var tbs = {
version: 'v1',
requestList: [ {
reqCert: certID
} ],
requestExtensions: [ {
extnID: rfc2560['id-pkix-ocsp-nonce'],
critical: false,
extnValue: rfc2560.Nonce.encode(crypto.randomBytes(16), 'der')
} ]
};

var req = {
tbsRequest: tbs
};

return {
id: sha1(rfc2560.CertID.encode(certID, 'der')),
data: rfc2560.OCSPRequest.encode(req, 'der'),

// Just to avoid re-parsing DER
cert: cert,
issuer: issuer
};
};
65 changes: 65 additions & 0 deletions lib/ocsp/utils.js
@@ -0,0 +1,65 @@
var http = require('http');
var url = require('url');
var rfc2560 = require('asn1.js-rfc2560');

exports.getResponse = function getResponse(uri, req, cb) {
uri = url.parse(uri);

var options = {
method: 'POST',
host: uri.host,
path: uri.path,
headers: {
'Content-Type': 'application/ocsp-request',
'Content-Length': req.length
}
};

http.request(options, onResponse)
.on('error', done)
.end(req);

function onResponse(response) {
if (response.statusCode < 200 || response.statusCode >= 400) {
return done(
new Error('Failed to obtain OCSP response: ' + response.statusCode));
}

var chunks = [];
response.on('readable', function() {
var chunk = response.read();
if (!chunk)
return;
chunks.push(chunk);
});
response.on('end', function() {
var ocsp = Buffer.concat(chunks);

done(null, ocsp);
});
}

function done(err, response) {
if (cb)
cb(err, response);
cb = null;
}
};

exports.parseResponse = function parseResponse(raw) {
var response = rfc2560.OCSPResponse.decode(raw, 'der');

var status = response.responseStatus;
if (status !== 'successful')
throw new Error('Bad OCSP response status: ' + status);

// Unknown response type
var responseType = response.responseBytes.responseType;
if (responseType !== 'id-pkix-ocsp-basic')
throw new Error('Unknown OCSP response type: ' + responseType);

var bytes = response.responseBytes.response;
var basic = rfc2560.BasicOCSPResponse.decode(bytes, 'der');

return basic;
};
25 changes: 25 additions & 0 deletions package.json
@@ -0,0 +1,25 @@
{
"name": "ocsp",
"version": "1.0.0",
"description": "OCSP Stapling implementation",
"main": "lib/ocsp.js",
"scripts": {
"test": "mocha test/*-test.js"
},
"keywords": [
"OCSP",
"ASN.1",
"Stapling"
],
"author": "Fedor Indutny <fedor@indutny.com>",
"license": "MIT",
"devDependencies": {
"mocha": "^2.2.5",
"selfsigned.js": "^1.1.0"
},
"dependencies": {
"asn1.js": "^2.0.3",
"asn1.js-rfc2560": "^2.1.0",
"asn1.js-rfc3280": "^2.1.0"
}
}

0 comments on commit d680b45

Please sign in to comment.