Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d680b45
Showing
12 changed files
with
538 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules/ | ||
npm-debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
exports.Cache = require('./ocsp/cache'); | ||
exports.request = require('./ocsp/request'); | ||
exports.check = require('./ocsp/check'); | ||
exports.utils = require('./ocsp/utils'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
Oops, something went wrong.