UNPKG

ioslib

Version:
1,457 lines (1,319 loc) 44 kB
/** * Detects iOS developer and distribution certificates and the WWDC certificate. * * @module certs * * @copyright * Copyright (c) 2014-2016 by Appcelerator, Inc. All Rights Reserved. * * Copyright (c) 2010-2014 Digital Bazaar, Inc. * {@link https://github.com/digitalbazaar/forge} * * @license * Licensed under the terms of the Apache Public License. * Please see the LICENSE included with this distribution for details. */ const appc = require('node-appc'), async = require('async'), env = require('./env'), magik = require('./utilities').magik, __ = appc.i18n(__dirname).__; const certRegExp = /^(?:((?:Apple|iOS) Development)|((?:iOS|Apple|iPhone) Distribution)): (.+)$/; var cache = null, watchers = {}, watchResults = null, watchInterval = 60000, watchTimer = null; exports.detect = detect; exports.watch = watch; exports.unwatch = unwatch; /** * Detects installed certificates. * * @param {Object} [options] - An object containing various settings. * @param {Boolean} [options.bypassCache=false] - When true, re-detects all certificates. * @param {Boolean} [options.validOnly=true] - When true, only returns non-expired, valid certificates. * @param {Function} [callback(err, results)] - A function to call with the certificate information. * * @emits module:certs#detected * @emits module:certs#error * * @returns {Handle} */ function detect(options, callback) { return magik(options, callback, function (emitter, options, callback) { var validOnly = options.validOnly === undefined || options.validOnly === true; if (cache && !options.bypassCache) { emitter.emit('detected', cache); return callback(null, cache); } function getCerts(cb) { // detect the development environment env.detect(options, function (err, env) { var results = { certs: { keychains: {}, wwdr: false }, issues: [] }; // if we don't have the security executable, we cannot detect certs if (!env.executables.security) { return cb(null, results); } appc.subprocess.run(env.executables.security, 'list-keychains', function (code, out, err) { if (code) { return cb(results); } function parseCerts(src, dest, prefix) { var p = 0, q = src.indexOf('-----END'), pem, cert, validity, expired, invalid, commonName; while (p !== -1 && q !== -1) { pem = src.substring(p, q + 25); cert = pem2cert(pem); expired = cert.validity.notAfter < now, invalid = expired || cert.validity.notBefore > now; commonName = cert.subject.getField('CN').value; let certName; if (!prefix) { const fullname = appc.encoding.decodeOctalUTF8(commonName); if (fullname === 'Apple Worldwide Developer Relations Certification Authority') { certName = commonName; } else { const match = fullname.match(certRegExp); if (match) { certName = match[3] } } } else { certName = appc.encoding.decodeOctalUTF8(commonName.substring(prefix.length)).trim(); } if (!validOnly || !invalid) { const teamId = cert.subject.attributes.find(attr => attr.name === 'organizationalUnitName'); dest.push({ name: certName, fullname: appc.encoding.decodeOctalUTF8(commonName).trim(), pem: pem, before: cert.validity.notBefore, after: cert.validity.notAfter, expired: expired, invalid: invalid, teamId: teamId && teamId.value }); } p = src.indexOf('-----BEGIN', q + 25); q = src.indexOf('-----END', p); } } var now = new Date, tasks = []; // parse out the keychains and add tasks to find certs for each keychain out.split('\n').forEach(function (line) { var m = line.match(/[^"]*"([^"]*)"/); if (!m) return; var keychain = m[1].trim(), dest = results.certs.keychains[keychain] = { developer: [], distribution: [] }; // find all the developer certificates in this keychain tasks.push(function (next) { appc.subprocess.run(env.executables.security, ['find-certificate', '-c', 'iPhone Developer:', '-a', '-p', keychain], function (code, out, err) { if (!code) { parseCerts(out, dest.developer, 'iPhone Developer:'); } next(); }); }); // find all the developer certificates in this keychain tasks.push(function (next) { appc.subprocess.run(env.executables.security, ['find-certificate', '-c', 'Development:', '-a', '-p', keychain], function (code, out, err) { if (!code) { parseCerts(out, dest.developer); } next(); }); }); // find all the distribution certificates in this keychain tasks.push(function (next) { appc.subprocess.run(env.executables.security, ['find-certificate', '-c', 'Distribution:', '-a', '-p', keychain], function (code, out, err) { if (!code) { parseCerts(out, dest.distribution); } next(); }); }); // find all the wwdr certificates in this keychain tasks.push(function (next) { // if we already found it, then skip the remaining keychains if (results.certs.wwdr) return next(); appc.subprocess.run(env.executables.security, ['find-certificate', '-c', 'Apple Worldwide Developer Relations Certification Authority', '-a', '-p', keychain], function (code, out, err) { if (!code) { var tmp = []; parseCerts(out, tmp); results.certs.wwdr = results.certs.wwdr || (tmp.length && tmp[0].invalid === false); } next(); }); }); }); // process all cert tasks async.parallel(tasks, function () { cb(results); }); }); }); } // get all keychains and certs getCerts(function (results) { detectIssues(results); cache = results; emitter.emit('detected', results); callback(null, results); }); }); }; function detectIssues(dest) { dest.issues = []; if (!dest.certs.wwdr) { dest.issues.push({ id: 'IOS_NO_WWDR_CERT_FOUND', type: 'error', message: __('Apple’s World Wide Developer Relations (WWDR) intermediate certificate is not installed.') + '\n' + __('This will prevent you from building apps for iOS devices or package for distribution.') }); } if (!Object.keys(dest.certs.keychains).length) { // I don't think this is even possible dest.issues.push({ id: 'IOS_NO_KEYCHAINS_FOUND', type: 'warning', message: __('Unable to find any keychains found.') }); } var validDevCerts = 0, validDistCerts = 0; Object.keys(dest.certs.keychains).forEach(function (keychain) { validDevCerts += (dest.certs.keychains[keychain].developer || []).filter(function (c) { return !c.invalid; }).length; validDistCerts += (dest.certs.keychains[keychain].distribution || []).filter(function (c) { return !c.invalid; }).length; }); if (!validDevCerts) { dest.issues.push({ id: 'IOS_NO_VALID_DEV_CERTS_FOUND', type: 'warning', message: __('Unable to find any valid iOS developer certificates.') + '\n' + __('This will prevent you from building apps for iOS devices.') }); } if (!validDistCerts) { dest.issues.push({ id: 'IOS_NO_VALID_DIST_CERTS_FOUND', type: 'warning', message: __('Unable to find any valid iOS production distribution certificates.') + '\n' + __('This will prevent you from packaging apps for distribution.') }); } } /** * Watches for new and changed certificates. * * @param {Object} [options] - An object containing various settings * @param {Boolean} [options.watchInterval=60000] - The number of milliseconds to wait before checking for cert updates * @param {Function} [callback(err, results)] - A function to call with the certificate information * * @returns {Function} A function that unwatches changes. */ function watch(options, callback) { if (typeof options === 'function') { callback = options; options = {}; } else if (!options) { options = {}; } watchers[callback] = (watchers[callback] || 0) + 1; watchInterval = ~~options.watchInterval || 60000; // check if already watching or already watching if (watchers[callback] === 1 && !watchTimer) { options.bypassCache = true; function check() { detect(options, function (err, results) { if (!err && (!watchResults || JSON.stringify(watchResults) !== JSON.stringify(results))) { watchResults = results; return callback(null, results); } watchTimer = setTimeout(check, watchInterval); }); } watchTimer = setTimeout(check, watchInterval); } return function () { unwatch(callback); }; }; /** * Stops watching for certificate changes. */ function unwatch(callback) { if (!watchers[callback]) return; if (--watchers[callback] <= 0) { delete watchers[callback]; } if (!Object.keys(watchers).length) { clearTimeout(watchTimer); watchTimer = null; } }; /* * Everything from this point onward is from the forge project (aka node-forge). * https://github.com/digitalbazaar/forge * * New BSD License (3-clause) * Copyright (c) 2010, Digital Bazaar, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Digital Bazaar, Inc. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var typeRegExp = /^(?:X509 |TRUSTED )?CERTIFICATE$/, rMessage = /\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\x21-\x7e\s]+?(?:\r?\n\r?\n))?([:A-Za-z0-9+\/=\s]+?)-----END \1-----/g, rHeader = /([\x21-\x7e]+):\s*([\x21-\x7e\s^:]+)/, rCRLF = /\r?\n/, whitespaceRegExp = /\s/, leadingSpaceRegExp = /^\s+/, asn1Class = { UNIVERSAL: 0x00, APPLICATION: 0x40, CONTEXT_SPECIFIC: 0x80, PRIVATE: 0xC0 }, asn1Type = { NONE: 0, BOOLEAN: 1, INTEGER: 2, BITSTRING: 3, OCTETSTRING: 4, NULL: 5, OID: 6, ODESC: 7, EXTERNAL: 8, REAL: 9, ENUMERATED: 10, EMBEDDED: 11, UTF8: 12, ROID: 13, SEQUENCE: 16, SET: 17, PRINTABLESTRING: 19, IA5STRING: 22, UTCTIME: 23, GENERALIZEDTIME: 24, BMPSTRING: 30 }, x509CertificateValidator = { name: 'Certificate', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, value: [ { name: 'Certificate.TBSCertificate', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, captureAsn1: 'tbsCertificate', value: [ { name: 'Certificate.TBSCertificate.version', tagClass: asn1Class.CONTEXT_SPECIFIC, type: 0, constructed: true, optional: true, value: [ { name: 'Certificate.TBSCertificate.version.integer', tagClass: asn1Class.UNIVERSAL, type: asn1Type.INTEGER, constructed: false, capture: 'certVersion' } ] }, { name: 'Certificate.TBSCertificate.serialNumber', tagClass: asn1Class.UNIVERSAL, type: asn1Type.INTEGER, constructed: false, capture: 'certSerialNumber' }, { name: 'Certificate.TBSCertificate.signature', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, value: [ { name: 'Certificate.TBSCertificate.signature.algorithm', tagClass: asn1Class.UNIVERSAL, type: asn1Type.OID, constructed: false, capture: 'certinfoSignatureOid' }, { name: 'Certificate.TBSCertificate.signature.parameters', tagClass: asn1Class.UNIVERSAL, optional: true, captureAsn1: 'certinfoSignatureParams' } ] }, { name: 'Certificate.TBSCertificate.issuer', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, captureAsn1: 'certIssuer' }, { name: 'Certificate.TBSCertificate.validity', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, // Note: UTC and generalized times may both appear so the capture // names are based on their detected order, the names used below // are only for the common case, which validity time really means // "notBefore" and which means "notAfter" will be determined by order value: [ { // notBefore (Time) (UTC time case) name: 'Certificate.TBSCertificate.validity.notBefore (utc)', tagClass: asn1Class.UNIVERSAL, type: asn1Type.UTCTIME, constructed: false, optional: true, capture: 'certValidity1UTCTime' }, { // notBefore (Time) (generalized time case) name: 'Certificate.TBSCertificate.validity.notBefore (generalized)', tagClass: asn1Class.UNIVERSAL, type: asn1Type.GENERALIZEDTIME, constructed: false, optional: true, capture: 'certValidity2GeneralizedTime' }, { // notAfter (Time) (only UTC time is supported) name: 'Certificate.TBSCertificate.validity.notAfter (utc)', tagClass: asn1Class.UNIVERSAL, type: asn1Type.UTCTIME, constructed: false, optional: true, capture: 'certValidity3UTCTime' }, { // notAfter (Time) (only UTC time is supported) name: 'Certificate.TBSCertificate.validity.notAfter (generalized)', tagClass: asn1Class.UNIVERSAL, type: asn1Type.GENERALIZEDTIME, constructed: false, optional: true, capture: 'certValidity4GeneralizedTime' } ] }, { // Name (subject) (RDNSequence) name: 'Certificate.TBSCertificate.subject', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, captureAsn1: 'certSubject' }, { name: 'SubjectPublicKeyInfo', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, captureAsn1: 'subjectPublicKeyInfo', value: [ { name: 'SubjectPublicKeyInfo.AlgorithmIdentifier', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, value: [ { name: 'AlgorithmIdentifier.algorithm', tagClass: asn1Class.UNIVERSAL, type: asn1Type.OID, constructed: false, capture: 'publicKeyOid' } ] }, { // subjectPublicKey name: 'SubjectPublicKeyInfo.subjectPublicKey', tagClass: asn1Class.UNIVERSAL, type: asn1Type.BITSTRING, constructed: false, value: [ { // RSAPublicKey name: 'SubjectPublicKeyInfo.subjectPublicKey.RSAPublicKey', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, optional: true, captureAsn1: 'rsaPublicKey' } ] } ] }, { // issuerUniqueID (optional) name: 'Certificate.TBSCertificate.issuerUniqueID', tagClass: asn1Class.CONTEXT_SPECIFIC, type: 1, constructed: true, optional: true, value: [ { name: 'Certificate.TBSCertificate.issuerUniqueID.id', tagClass: asn1Class.UNIVERSAL, type: asn1Type.BITSTRING, constructed: false, capture: 'certIssuerUniqueId' } ] }, { // subjectUniqueID (optional) name: 'Certificate.TBSCertificate.subjectUniqueID', tagClass: asn1Class.CONTEXT_SPECIFIC, type: 2, constructed: true, optional: true, value: [ { name: 'Certificate.TBSCertificate.subjectUniqueID.id', tagClass: asn1Class.UNIVERSAL, type: asn1Type.BITSTRING, constructed: false, capture: 'certSubjectUniqueId' } ] }, { // Extensions (optional) name: 'Certificate.TBSCertificate.extensions', tagClass: asn1Class.CONTEXT_SPECIFIC, type: 3, constructed: true, captureAsn1: 'certExtensions', optional: true } ] }, { // AlgorithmIdentifier (signature algorithm) name: 'Certificate.signatureAlgorithm', tagClass: asn1Class.UNIVERSAL, type: asn1Type.SEQUENCE, constructed: true, value: [ { // algorithm name: 'Certificate.signatureAlgorithm.algorithm', tagClass: asn1Class.UNIVERSAL, type: asn1Type.OID, constructed: false, capture: 'certSignatureOid' }, { name: 'Certificate.TBSCertificate.signature.parameters', tagClass: asn1Class.UNIVERSAL, optional: true, captureAsn1: 'certSignatureParams' } ] }, { // SignatureValue name: 'Certificate.signatureValue', tagClass: asn1Class.UNIVERSAL, type: asn1Type.BITSTRING, constructed: false, capture: 'certSignature' } ] }, oids = { // algorithm OIDs '1.2.840.113549.1.1.1': 'rsaEncryption', 'rsaEncryption': '1.2.840.113549.1.1.1', // Note: md2 & md4 not implemented //'1.2.840.113549.1.1.2': 'md2WithRSAEncryption', //'md2WithRSAEncryption': '1.2.840.113549.1.1.2', //'1.2.840.113549.1.1.3': 'md4WithRSAEncryption', //'md4WithRSAEncryption': '1.2.840.113549.1.1.3', '1.2.840.113549.1.1.4': 'md5WithRSAEncryption', 'md5WithRSAEncryption': '1.2.840.113549.1.1.4', '1.2.840.113549.1.1.5': 'sha1WithRSAEncryption', 'sha1WithRSAEncryption': '1.2.840.113549.1.1.5', '1.2.840.113549.1.1.7': 'RSAES-OAEP', 'RSAES-OAEP': '1.2.840.113549.1.1.7', '1.2.840.113549.1.1.8': 'mgf1', 'mgf1': '1.2.840.113549.1.1.8', '1.2.840.113549.1.1.9': 'pSpecified', 'pSpecified': '1.2.840.113549.1.1.9', '1.2.840.113549.1.1.10': 'RSASSA-PSS', 'RSASSA-PSS': '1.2.840.113549.1.1.10', '1.2.840.113549.1.1.11': 'sha256WithRSAEncryption', 'sha256WithRSAEncryption': '1.2.840.113549.1.1.11', '1.2.840.113549.1.1.12': 'sha384WithRSAEncryption', 'sha384WithRSAEncryption': '1.2.840.113549.1.1.12', '1.2.840.113549.1.1.13': 'sha512WithRSAEncryption', 'sha512WithRSAEncryption': '1.2.840.113549.1.1.13', '1.3.14.3.2.7': 'desCBC', 'desCBC': '1.3.14.3.2.7', '1.3.14.3.2.26': 'sha1', 'sha1': '1.3.14.3.2.26', '2.16.840.1.101.3.4.2.1': 'sha256', 'sha256': '2.16.840.1.101.3.4.2.1', '2.16.840.1.101.3.4.2.2': 'sha384', 'sha384': '2.16.840.1.101.3.4.2.2', '2.16.840.1.101.3.4.2.3': 'sha512', 'sha512': '2.16.840.1.101.3.4.2.3', '1.2.840.113549.2.5': 'md5', 'md5': '1.2.840.113549.2.5', // pkcs#7 content types '1.2.840.113549.1.7.1': 'data', 'data': '1.2.840.113549.1.7.1', '1.2.840.113549.1.7.2': 'signedData', 'signedData': '1.2.840.113549.1.7.2', '1.2.840.113549.1.7.3': 'envelopedData', 'envelopedData': '1.2.840.113549.1.7.3', '1.2.840.113549.1.7.4': 'signedAndEnvelopedData', 'signedAndEnvelopedData': '1.2.840.113549.1.7.4', '1.2.840.113549.1.7.5': 'digestedData', 'digestedData': '1.2.840.113549.1.7.5', '1.2.840.113549.1.7.6': 'encryptedData', 'encryptedData': '1.2.840.113549.1.7.6', // pkcs#9 oids '1.2.840.113549.1.9.1': 'emailAddress', 'emailAddress': '1.2.840.113549.1.9.1', '1.2.840.113549.1.9.2': 'unstructuredName', 'unstructuredName': '1.2.840.113549.1.9.2', '1.2.840.113549.1.9.3': 'contentType', 'contentType': '1.2.840.113549.1.9.3', '1.2.840.113549.1.9.4': 'messageDigest', 'messageDigest': '1.2.840.113549.1.9.4', '1.2.840.113549.1.9.5': 'signingTime', 'signingTime': '1.2.840.113549.1.9.5', '1.2.840.113549.1.9.6': 'counterSignature', 'counterSignature': '1.2.840.113549.1.9.6', '1.2.840.113549.1.9.7': 'challengePassword', 'challengePassword': '1.2.840.113549.1.9.7', '1.2.840.113549.1.9.8': 'unstructuredAddress', 'unstructuredAddress': '1.2.840.113549.1.9.8', '1.2.840.113549.1.9.20': 'friendlyName', 'friendlyName': '1.2.840.113549.1.9.20', '1.2.840.113549.1.9.21': 'localKeyId', 'localKeyId': '1.2.840.113549.1.9.21', '1.2.840.113549.1.9.22.1': 'x509Certificate', 'x509Certificate': '1.2.840.113549.1.9.22.1', // pkcs#12 safe bags '1.2.840.113549.1.12.10.1.1': 'keyBag', 'keyBag': '1.2.840.113549.1.12.10.1.1', '1.2.840.113549.1.12.10.1.2': 'pkcs8ShroudedKeyBag', 'pkcs8ShroudedKeyBag': '1.2.840.113549.1.12.10.1.2', '1.2.840.113549.1.12.10.1.3': 'certBag', 'certBag': '1.2.840.113549.1.12.10.1.3', '1.2.840.113549.1.12.10.1.4': 'crlBag', 'crlBag': '1.2.840.113549.1.12.10.1.4', '1.2.840.113549.1.12.10.1.5': 'secretBag', 'secretBag': '1.2.840.113549.1.12.10.1.5', '1.2.840.113549.1.12.10.1.6': 'safeContentsBag', 'safeContentsBag': '1.2.840.113549.1.12.10.1.6', // password-based-encryption for pkcs#12 '1.2.840.113549.1.5.13': 'pkcs5PBES2', 'pkcs5PBES2': '1.2.840.113549.1.5.13', '1.2.840.113549.1.5.12': 'pkcs5PBKDF2', 'pkcs5PBKDF2': '1.2.840.113549.1.5.12', '1.2.840.113549.1.12.1.1': 'pbeWithSHAAnd128BitRC4', 'pbeWithSHAAnd128BitRC4': '1.2.840.113549.1.12.1.1', '1.2.840.113549.1.12.1.2': 'pbeWithSHAAnd40BitRC4', 'pbeWithSHAAnd40BitRC4': '1.2.840.113549.1.12.1.2', '1.2.840.113549.1.12.1.3': 'pbeWithSHAAnd3-KeyTripleDES-CBC', 'pbeWithSHAAnd3-KeyTripleDES-CBC': '1.2.840.113549.1.12.1.3', '1.2.840.113549.1.12.1.4': 'pbeWithSHAAnd2-KeyTripleDES-CBC', 'pbeWithSHAAnd2-KeyTripleDES-CBC': '1.2.840.113549.1.12.1.4', '1.2.840.113549.1.12.1.5': 'pbeWithSHAAnd128BitRC2-CBC', 'pbeWithSHAAnd128BitRC2-CBC': '1.2.840.113549.1.12.1.5', '1.2.840.113549.1.12.1.6': 'pbewithSHAAnd40BitRC2-CBC', 'pbewithSHAAnd40BitRC2-CBC': '1.2.840.113549.1.12.1.6', // symmetric key algorithm oids '1.2.840.113549.3.7': 'des-EDE3-CBC', 'des-EDE3-CBC': '1.2.840.113549.3.7', '2.16.840.1.101.3.4.1.2': 'aes128-CBC', 'aes128-CBC': '2.16.840.1.101.3.4.1.2', '2.16.840.1.101.3.4.1.22': 'aes192-CBC', 'aes192-CBC': '2.16.840.1.101.3.4.1.22', '2.16.840.1.101.3.4.1.42': 'aes256-CBC', 'aes256-CBC': '2.16.840.1.101.3.4.1.42', // certificate issuer/subject OIDs '2.5.4.3': 'commonName', 'commonName': '2.5.4.3', '2.5.4.5': 'serialName', 'serialName': '2.5.4.5', '2.5.4.6': 'countryName', 'countryName': '2.5.4.6', '2.5.4.7': 'localityName', 'localityName': '2.5.4.7', '2.5.4.8': 'stateOrProvinceName', 'stateOrProvinceName': '2.5.4.8', '2.5.4.10': 'organizationName', 'organizationName': '2.5.4.10', '2.5.4.11': 'organizationalUnitName', 'organizationalUnitName': '2.5.4.11', // X.509 extension OIDs '2.16.840.1.113730.1.1': 'nsCertType', 'nsCertType': '2.16.840.1.113730.1.1', '2.5.29.1': 'authorityKeyIdentifier', // deprecated, use .35 '2.5.29.2': 'keyAttributes', // obsolete use .37 or .15 '2.5.29.3': 'certificatePolicies', // deprecated, use .32 '2.5.29.4': 'keyUsageRestriction', // obsolete use .37 or .15 '2.5.29.5': 'policyMapping', // deprecated use .33 '2.5.29.6': 'subtreesConstraint', // obsolete use .30 '2.5.29.7': 'subjectAltName', // deprecated use .17 '2.5.29.8': 'issuerAltName', // deprecated use .18 '2.5.29.9': 'subjectDirectoryAttributes', '2.5.29.10': 'basicConstraints', // deprecated use .19 '2.5.29.11': 'nameConstraints', // deprecated use .30 '2.5.29.12': 'policyConstraints', // deprecated use .36 '2.5.29.13': 'basicConstraints', // deprecated use .19 '2.5.29.14': 'subjectKeyIdentifier', 'subjectKeyIdentifier': '2.5.29.14', '2.5.29.15': 'keyUsage', 'keyUsage': '2.5.29.15', '2.5.29.16': 'privateKeyUsagePeriod', '2.5.29.17': 'subjectAltName', 'subjectAltName': '2.5.29.17', '2.5.29.18': 'issuerAltName', 'issuerAltName': '2.5.29.18', '2.5.29.19': 'basicConstraints', 'basicConstraints': '2.5.29.19', '2.5.29.20': 'cRLNumber', '2.5.29.21': 'cRLReason', '2.5.29.22': 'expirationDate', '2.5.29.23': 'instructionCode', '2.5.29.24': 'invalidityDate', '2.5.29.25': 'cRLDistributionPoints', // deprecated use .31 '2.5.29.26': 'issuingDistributionPoint', // deprecated use .28 '2.5.29.27': 'deltaCRLIndicator', '2.5.29.28': 'issuingDistributionPoint', '2.5.29.29': 'certificateIssuer', '2.5.29.30': 'nameConstraints', '2.5.29.31': 'cRLDistributionPoints', '2.5.29.32': 'certificatePolicies', '2.5.29.33': 'policyMappings', '2.5.29.34': 'policyConstraints', // deprecated use .36 '2.5.29.35': 'authorityKeyIdentifier', '2.5.29.36': 'policyConstraints', '2.5.29.37': 'extKeyUsage', 'extKeyUsage': '2.5.29.37', '2.5.29.46': 'freshestCRL', '2.5.29.54': 'inhibitAnyPolicy', // extKeyUsage purposes '1.3.6.1.5.5.7.3.1': 'serverAuth', 'serverAuth': '1.3.6.1.5.5.7.3.1', '1.3.6.1.5.5.7.3.2': 'clientAuth', 'clientAuth': '1.3.6.1.5.5.7.3.2', '1.3.6.1.5.5.7.3.3': 'codeSigning', 'codeSigning': '1.3.6.1.5.5.7.3.3', '1.3.6.1.5.5.7.3.4': 'emailProtection', 'emailProtection': '1.3.6.1.5.5.7.3.4', '1.3.6.1.5.5.7.3.8': 'timeStamping', 'timeStamping': '1.3.6.1.5.5.7.3.8' }, shortNames = { 'CN': oids['commonName'], 'commonName': 'CN', 'C': oids['countryName'], 'countryName': 'C', 'L': oids['localityName'], 'localityName': 'L', 'ST': oids['stateOrProvinceName'], 'stateOrProvinceName': 'ST', 'O': oids['organizationName'], 'organizationName': 'O', 'OU': oids['organizationalUnitName'], 'organizationalUnitName': 'OU', 'E': oids['emailAddress'], 'emailAddress': 'E' }; function pem2cert(pem) { var msg = decodePem(pem)[0]; if (msg.type !== 'CERTIFICATE' && msg.type !== 'X509 CERTIFICATE' && msg.type !== 'TRUSTED CERTIFICATE') { throw new Error(__('Could not convert certificate from PEM; PEM header type is "%s", but must be "CERTIFICATE", "X509 CERTIFICATE", or "TRUSTED CERTIFICATE".', msg.type)); } if (msg.procType && msg.procType.type === 'ENCRYPTED') { throw new Error(__('Could not convert certificate from PEM; PEM is encrypted.')); } return asn2cert(der2asn(new ByteStringBuffer(msg.body))); } function decodePem(str) { var rval = [], match, msg, lines, li, line, nl, next, header, values, vi; while (true) { match = rMessage.exec(str); if (!match) { break; } rval.push(msg = { type: match[1], procType: null, contentDomain: null, dekInfo: null, headers: [], body: Buffer.from(match[3], 'base64').toString('binary') }); // no headers if (!match[2]) { continue; } // parse headers lines = match[2].split(rCRLF); for (li = 0; match && li < lines.length; ++li) { // get line, trim any rhs whitespace line = lines[li].replace(/\s+$/, ''); // RFC2822 unfold any following folded lines for (nl = li + 1; nl < lines.length; ++nl) { next = lines[nl]; if (!whitespaceRegExp.test(next[0])) { break; } line += next; li = nl; } // parse header match = line.match(rHeader); if (match) { header = {name: match[1], values: []}; values = match[2].split(','); for (vi = 0; vi < values.length; ++vi) { header.values.push(values[vi].replace(leadingSpaceRegExp, '')); } // Proc-Type must be the first header if (!msg.procType) { if (header.name !== 'Proc-Type') { throw new Error(__('Invalid PEM formatted message. The first encapsulated header must be "Proc-Type".')); } else if (header.values.length !== 2) { throw new Error(__('Invalid PEM formatted message. The "Proc-Type" header must have two subfields.')); } msg.procType = { version: values[0], type: values[1] }; // special-case Content-Domain } else if (!msg.contentDomain && header.name === 'Content-Domain') { msg.contentDomain = values[0] || ''; // special-case DEK-Info } else if (!msg.dekInfo && header.name === 'DEK-Info') { if (header.values.length === 0) { throw new Error(__('Invalid PEM formatted message. The "DEK-Info" header must have at least one subfield.')); } msg.dekInfo = { algorithm: values[0], parameters: values[1] || null }; } else { msg.headers.push(header); } } } if (msg.procType === 'ENCRYPTED' && !msg.dekInfo) { throw new Error(__('Invalid PEM formatted message. The "DEK-Info" header must be present if "Proc-Type" is "ENCRYPTED".')); } } if (rval.length === 0) { throw new Error(__('Invalid PEM formatted message.')); } return rval; } function ByteStringBuffer(str) { this.data = str; this.read = 0; } ByteStringBuffer.prototype.length = function length() { return this.data.length - this.read; }; ByteStringBuffer.prototype.getByte = function getByte() { return this.data.charCodeAt(this.read++); }; ByteStringBuffer.prototype.getInt = function getInt(n) { var rval = 0; do { rval = (rval << 8) + this.data.charCodeAt(this.read++); n -= 8; } while (n > 0); return rval; }; ByteStringBuffer.prototype.bytes = function bytes(count) { return count === undefined ? this.data.slice(this.read) : this.data.slice(this.read, this.read + count); }; ByteStringBuffer.prototype.getBytes = function getBytes(count) { var rval; if (count) { // read count bytes count = Math.min(this.length(), count); rval = this.data.slice(this.read, this.read + count); this.read += count; } else if (count === 0) { rval = ''; } else { // read all bytes, optimize to only copy when needed rval = this.read === 0 ? this.data : this.data.slice(this.read); this.clear(); } return rval; }; ByteStringBuffer.prototype.getInt16 = function getInt16() { var rval = (this.data.charCodeAt(this.read) << 8 ^ this.data.charCodeAt(this.read + 1)); this.read += 2; return rval; }; ByteStringBuffer.prototype.clear = function clear() { this.data = ''; this.read = 0; return this; }; function der2asn(bytes) { // minimum length for ASN.1 DER structure is 2 if (bytes.length() < 2) { throw new Error(__('Too few bytes to parse DER; expected at least 2, got %d', bytes.length())); } // get the first byte var b1 = bytes.getByte(), // get the tag class tagClass = (b1 & 0xC0), // get the type (bits 1-5) type = b1 & 0x1F, _getValueLength = function _getValueLength(b) { var b2 = b.getByte(); if (b2 === 0x80) { return undefined; } // see if the length is "short form" or "long form" (bit 8 set) // if "long form", the number of bytes the length is specified in bits 7 through 1 // and each length byte is in big-endian base-256 return b2 & 0x80 ? b.getInt((b2 & 0x7F) << 3) : b2; }, // get the value length length = _getValueLength(bytes), // prepare to get value value, // constructed flag is bit 6 (32 = 0x20) of the first byte constructed = ((b1 & 0x20) === 0x20), composed = constructed; // ensure there are enough bytes to get the value if (bytes.length() < length) { throw new Error(__('Too few bytes to read ASN.1 value. %d < %d', bytes.length(), length)); } // determine if the value is composed of other ASN.1 objects (if its // constructed it will be and if its a BITSTRING it may be) if (!composed && tagClass === asn1Class.UNIVERSAL && type === asn1Type.BITSTRING && length > 1) { /* The first octet gives the number of bits by which the length of the bit string is less than the next multiple of eight (this is called the "number of unused bits"). The second and following octets give the value of the bit string converted to an octet string. */ // if there are no unused bits, maybe the bitstring holds ASN.1 objs var read = bytes.read, unused = bytes.getByte(); if (unused === 0) { // if the first byte indicates UNIVERSAL or CONTEXT_SPECIFIC, // and the length is valid, assume we've got an ASN.1 object b1 = bytes.getByte(); var tc = (b1 & 0xC0); if (tc === asn1Class.UNIVERSAL || tc === asn1Class.CONTEXT_SPECIFIC) { try { var len = _getValueLength(bytes); composed = (len === length - (bytes.read - read)); if (composed) { // adjust read/length to account for unused bits byte ++read; --length; } } catch(ex) {} } } // restore read pointer bytes.read = read; } if (composed) { // parse child asn1 objects from the value value = []; if (length === undefined) { // asn1 object of indefinite length, read until end tag for (;;) { if (bytes.bytes(2) === String.fromCharCode(0, 0)) { bytes.getBytes(2); break; } value.push(der2asn(bytes)); } } else { // parsing asn1 object of definite length var start = bytes.length(); while (length > 0) { value.push(der2asn(bytes)); length -= start - bytes.length(); start = bytes.length(); } } } else { // asn1 not composed, get raw value // TODO: do DER to OID conversion and vice-versa in .toDer? if (length === undefined) { throw new Error(__('Non-constructed ASN.1 object of indefinite length.')); } if (type === asn1Type.BMPSTRING) { value = ''; for (var i = 0; i < length; i += 2) { value += String.fromCharCode(bytes.getInt16()); } } else { value = bytes.getBytes(length); } } return { tagClass: tagClass, type: type, constructed: constructed, composed: constructed || Array.isArray(value), value: Array.isArray(value) ? value.filter(function (v) { return v !== undefined; }) : value }; } function asn1validate(obj, v, capture, errors) { var rval = false; // ensure tag class and type are the same if specified if ((obj.tagClass === v.tagClass || v.tagClass === undefined) && (obj.type === v.type || v.type === undefined)) { // ensure constructed flag is the same if specified if (obj.constructed === v.constructed || v.constructed === undefined) { rval = true; // handle sub values if (v.value && Array.isArray(v.value)) { var j = 0; for (var i = 0; rval && i < v.value.length; ++i) { rval = v.value[i].optional || false; if (obj.value[j]) { rval = asn1validate(obj.value[j], v.value[i], capture, errors); if (rval) { ++j; } else if (v.value[i].optional) { rval = true; } } if (!rval && errors) { errors.push('[' + v.name + '] Tag class "' + v.tagClass + '", type "' + v.type + '" expected value length "' + v.value.length + '", got "' + obj.value.length + '"'); } } } if (rval && capture) { if (v.capture) { capture[v.capture] = obj.value; } if (v.captureAsn1) { capture[v.captureAsn1] = obj; } } } else if (errors) { errors.push('[' + v.name + '] Expected constructed "' + v.constructed + '", got "' + obj.constructed + '"'); } } else if (errors) { if (obj.tagClass !== v.tagClass) { errors.push('[' + v.name + '] Expected tag class "' + v.tagClass + '", got "' + obj.tagClass + '"'); } if (obj.type !== v.type) { errors.push('[' + v.name + '] Expected type "' + v.type + '", got "' + obj.type + '"'); } } return rval; } /** * Converts a UTCTime value to a date. * * Note: GeneralizedTime has 4 digits for the year and is used for X.509 * dates passed 2049. Parsing that structure hasn't been implemented yet. * * @param utc the UTCTime value to convert. * * @return the date. */ function asn1utcTimeToDate(utc) { /* The following formats can be used: YYMMDDhhmmZ YYMMDDhhmm+hh'mm' YYMMDDhhmm-hh'mm' YYMMDDhhmmssZ YYMMDDhhmmss+hh'mm' YYMMDDhhmmss-hh'mm' Where: YY is the least significant two digits of the year MM is the month (01 to 12) DD is the day (01 to 31) hh is the hour (00 to 23) mm are the minutes (00 to 59) ss are the seconds (00 to 59) Z indicates that local time is GMT, + indicates that local time is later than GMT, and - indicates that local time is earlier than GMT hh' is the absolute value of the offset from GMT in hours mm' is the absolute value of the offset from GMT in minutes */ var date = new Date; // if YY >= 50 use 19xx, if YY < 50 use 20xx var year = parseInt(utc.substr(0, 2), 10); year = (year >= 50) ? 1900 + year : 2000 + year; var MM = parseInt(utc.substr(2, 2), 10) - 1; // use 0-11 for month var DD = parseInt(utc.substr(4, 2), 10); var hh = parseInt(utc.substr(6, 2), 10); var mm = parseInt(utc.substr(8, 2), 10); var ss = 0; // not just YYMMDDhhmmZ if (utc.length > 11) { // get character after minutes var c = utc.charAt(10); var end = 10; // see if seconds are present if (c !== '+' && c !== '-') { // get seconds ss = parseInt(utc.substr(10, 2), 10); end += 2; } } // update date date.setUTCFullYear(year, MM, DD); date.setUTCHours(hh, mm, ss, 0); if (end) { // get +/- after end of time c = utc.charAt(end); if (c === '+' || c === '-') { // get hours+minutes offset var hhoffset = parseInt(utc.substr(end + 1, 2), 10); var mmoffset = parseInt(utc.substr(end + 4, 2), 10); // calculate offset in milliseconds var offset = hhoffset * 60 + mmoffset; offset *= 60000; // apply offset if (c === '+') { date.setTime(+date - offset); } else { date.setTime(+date + offset); } } } return date; } /** * Converts a GeneralizedTime value to a date. * * @param gentime the GeneralizedTime value to convert. * * @return the date. */ function asn1generalizedTimeToDate(gentime) { /* The following formats can be used: YYYYMMDDHHMMSS YYYYMMDDHHMMSS.fff YYYYMMDDHHMMSSZ YYYYMMDDHHMMSS.fffZ YYYYMMDDHHMMSS+hh'mm' YYYYMMDDHHMMSS.fff+hh'mm' YYYYMMDDHHMMSS-hh'mm' YYYYMMDDHHMMSS.fff-hh'mm' Where: YYYY is the year MM is the month (01 to 12) DD is the day (01 to 31) hh is the hour (00 to 23) mm are the minutes (00 to 59) ss are the seconds (00 to 59) .fff is the second fraction, accurate to three decimal places Z indicates that local time is GMT, + indicates that local time is later than GMT, and - indicates that local time is earlier than GMT hh' is the absolute value of the offset from GMT in hours mm' is the absolute value of the offset from GMT in minutes */ var date = new Date, YYYY = parseInt(gentime.substr(0, 4), 10), MM = parseInt(gentime.substr(4, 2), 10) - 1, // use 0-11 for month DD = parseInt(gentime.substr(6, 2), 10), hh = parseInt(gentime.substr(8, 2), 10), mm = parseInt(gentime.substr(10, 2), 10), ss = parseInt(gentime.substr(12, 2), 10), fff = 0, offset = 0, isUTC = false; if (gentime.charAt(gentime.length - 1) === 'Z') { isUTC = true; } var end = gentime.length - 5, c = gentime.charAt(end); if (c === '+' || c === '-') { // get hours+minutes offset var hhoffset = parseInt(gentime.substr(end + 1, 2), 10); var mmoffset = parseInt(gentime.substr(end + 4, 2), 10); // calculate offset in milliseconds offset = hhoffset * 60 + mmoffset; offset *= 60000; // apply offset if(c === '+') { offset *= -1; } isUTC = true; } // check for second fraction if(gentime.charAt(14) === '.') { fff = parseFloat(gentime.substr(14), 10) * 1000; } if(isUTC) { date.setUTCFullYear(YYYY, MM, DD); date.setUTCHours(hh, mm, ss, fff); // apply offset date.setTime(+date + offset); } else { date.setFullYear(YYYY, MM, DD); date.setHours(hh, mm, ss, fff); } return date; } /** * Converts a DER-encoded byte buffer to an OID dot-separated string. The * byte buffer should contain only the DER-encoded value, not any tag or * length bytes. * * @param bytes the byte buffer. * * @return the OID dot-separated string. */ function asn1derToOid(bytes) { var oid; // wrap in buffer if needed if (typeof bytes === 'string') { bytes = new ByteStringBuffer(bytes); } // first byte is 40 * value1 + value2 var b = bytes.getByte(); oid = Math.floor(b / 40) + '.' + (b % 40); // other bytes are each value in base 128 with 8th bit set except for // the last byte for each value var value = 0; while (bytes.length() > 0) { b = bytes.getByte(); value = value << 7; // not the last byte for the value if (b & 0x80) { value += b & 0x7F; } else { // last byte oid += '.' + (value + b); value = 0; } } return oid; } /** * Converts an RDNSequence of ASN.1 DER-encoded RelativeDistinguishedName * sets into an array with objects that have type and value properties. * * @param rdn the RDNSequence to convert. * @param md a message digest to append type and value to if provided. */ function pkiRDNAttributesAsArray(rdn, md) { // each value in 'rdn' in is a SET of RelativeDistinguishedName var rval = [], si, i, set, attr, obj; for (si = 0; si < rdn.value.length; ++si) { // get the RelativeDistinguishedName set set = rdn.value[si]; // each value in the SET is an AttributeTypeAndValue sequence // containing first a type (an OID) and second a value (defined by // the OID) for (i = 0; i < set.value.length; ++i) { obj = {}; attr = set.value[i]; obj.type = asn1derToOid(attr.value[0].value); obj.value = attr.value[1].value; obj.valueTagClass = attr.value[1].type; // if the OID is known, get its name and short name if (obj.type in oids) { obj.name = oids[obj.type]; if (obj.name in shortNames) { obj.shortName = shortNames[obj.name]; } } if (md) { md.update(obj.type); md.update(obj.value); } rval.push(obj); } } return rval; } function asn2cert(obj) { // validate certificate and capture data var capture = {}, errors = []; if (!asn1validate(obj, x509CertificateValidator, capture, errors)) { var error = new Error(__('Cannot read X.509 certificate. ASN.1 object is not an X509v3 Certificate.')); error.errors = errors; throw error; } function _getAttribute(obj, options) { if (typeof options === 'string') { options = { shortName: options }; } var rval = null, attr; for (var i = 0; rval === null && i < obj.attributes.length; ++i) { attr = obj.attributes[i]; if (options.type && options.type === attr.type) { rval = attr; } else if (options.name && options.name === attr.name) { rval = attr; } else if (options.shortName && options.shortName === attr.shortName) { rval = attr; } } return rval; } var subject = { attributes: pkiRDNAttributesAsArray(capture.certSubject), getField: function (sn) { return _getAttribute(subject, sn); } }, validity = []; if (capture.certValidity1UTCTime !== undefined) { validity.push(asn1utcTimeToDate(capture.certValidity1UTCTime)); } if (capture.certValidity2GeneralizedTime !== undefined) { validity.push(asn1generalizedTimeToDate(capture.certValidity2GeneralizedTime)); } if (capture.certValidity3UTCTime !== undefined) { validity.push(asn1utcTimeToDate(capture.certValidity3UTCTime)); } if (capture.certValidity4GeneralizedTime !== undefined) { validity.push(asn1generalizedTimeToDate(capture.certValidity4GeneralizedTime)); } if (validity.length > 2) { throw new Error(__('Cannot read notBefore/notAfter validity times; more than two times were provided in the certificate.')); } if (validity.length < 2) { throw new Error(__('Cannot read notBefore/notAfter validity times; they were not provided as either UTCTime or GeneralizedTime.')); } return { validity: { notBefore: validity[0], notAfter: validity[1] }, subject: subject }; } /* * If the app exits, close all filesystem watchers. */ process.on('exit', function () { if (watchTimer) { clearTimeout(watchTimer); watchTimer = null; } });