diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..beadc5f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,25 @@ +The MIT License (MIT) +===================== + +Copyright © 2016 Yury Popov + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4418f0 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Certificate common names extract library + +[![NPM version](https://badge.fury.io/js/certnames.svg)](https://www.npmjs.com/package/certnames) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) + +This module provides API to get SSL certificate common names list. + +## Usage + +```javascript +const certnames = require('certnames') +const tls = require('tls') + +const host = 'comodo.com' +const sock = tls.connect(443, host) + +sock.on('secureConnect', () => { + const cert = sock.getPeerCertificate().raw + const names = certnames.getCommonNames(cert) + console.log('Common names: %j', names) + // Common names: ["ssl383141.cloudflaressl.com","*.comodo.com","comodo.com"] + const regex = certnames.toRegEx(names) + console.log('Regex: %s', regex) + // Regex: /^((ssl383141\.cloudflaressl\.com)|(([^\.]+\.)?(comodo\.com)))$/gm +}) +``` + +## API + +### getCommonNames(buffer[, encoding]) +Extracts common names (with "alternative" if present) from buffer. Both `der` and `pem` encodings supported. + +### toRegEx(names) +Generates regular expression that match any of common name extracted before. Supports RegEx simplification, so wildcard and non-wildcard domains are grouped into single expression. + +## Thanks to +* [Fedor Indutny](https://github.com/indutny)
+ For powerful ASN.1 parser library + +## LICENSE +This software is licensed under the MIT License. + +Copyright Yury Popov _a.k.a. [PhoeniX](https://phoenix.dj)_, 2016. + +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. \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..856f4d2 --- /dev/null +++ b/index.js @@ -0,0 +1,88 @@ +'use strict' + +const asn1 = require('asn1.js') +const asn1Cert = require('asn1.js-rfc3280') + +const ASNStr = asn1.define('ASNStr', function () { + this.choice({ + bit: this.bitstr(), + bmp: this.bmpstr(), + chr: this.charstr(), + gen: this.genstr(), + grp: this.graphstr(), + ia5: this.ia5str(), + iso: this.iso646str(), + num: this.numstr(), + oct: this.octstr(), + prt: this.printstr(), + t61: this.t61str(), + uni: this.unistr(), + utf: this.utf8str(), + vid: this.videostr() + }) +}) + +function getCommonNames (buffer, encoding) { + if (encoding === undefined) { + encoding = (/^\s*-----\s*BEGIN/.test(buffer.toString())) ? 'pem' : 'der' + } + const cert = asn1Cert.Certificate.decode(buffer, encoding, {label: 'CERTIFICATE'}) + var names = [] + cert.tbsCertificate.subject.value.forEach((e) => { + if (e[0].type.join('.') !== '2.5.4.3') return + names.push(ASNStr.decode(e[0].value).value) + }) + cert.tbsCertificate.extensions.forEach((e) => { + if (e.extnID.join('.') !== '2.5.29.17') return + names.push.apply(names, asn1Cert.GeneralNames.decode(e.extnValue).map((e) => { + switch (e.type) { + case 'dNSName': + return e.value + case 'iPAddress': + return [e.value[0], e.value[1], e.value[2], e.value[3]].join('.') + default: + console.log('Unknown type: %s', e.type) + return null + } + })) + }) + return names.filter((e, i, s) => { + if (e === null) return false + return s.indexOf(e) === i + }) +} + +function toRegEx (names) { + const domains = { wc: [], single: [], both: [] } + names.forEach((e) => { + var wc = false + if (e[0] === '*') { + wc = true + e = e.split('.').splice(1).join('.') + } + e = e.replace(/\./g, '\\.') + if (wc) domains.wc.push(e) + else domains.single.push(e) + }) + domains.wc = domains.wc.filter((e) => { + const idx = domains.single.indexOf(e) + if (idx === -1) return true + domains.single.splice(idx, 1) + domains.both.push(e) + return false + }) + const entries = [] + if (domains.single.length > 0) { + entries.push(domains.single.join('|')) + } + if (domains.wc.length > 0) { + entries.push('[^\\.]+\\.(' + domains.wc.join('|') + ')') + } + if (domains.both.length > 0) { + entries.push('([^\\.]+\\.)?(' + domains.both.join('|') + ')') + } + return new RegExp('^((' + entries.join(')|(') + '))$', 'gm') +} + +exports.getCommonNames = getCommonNames +exports.toRegEx = toRegEx diff --git a/package.json b/package.json new file mode 100644 index 0000000..c923514 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "certnames", + "version": "1.0.0", + "description": "Export certificate common names from buffer", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://git.phoenix.dj/phoenix/node-certnames.git" + }, + "keywords": [ + "x509", + "certificate", + "asn1" + ], + "author": "PhoeniX", + "license": "MIT", + "dependencies": { + "asn1.js": "^4.6.2", + "asn1.js-rfc3280": "^4.0.0" + } +}