diff --git a/CHANGELOG.md b/CHANGELOG.md index a302bc64..a21ac5da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ All notable changes to the Zlux Server Framework package will be documented in this file. This repo is part of the app-server Zowe Component, and the change logs here may appear on Zowe.org in that section. +## 3.6.0 +- Enhancement: When not using a SAF keyring, the app-server can now use PKCS12 keystores so that the PEM option for Zowe certificates no longer needs to be specified. [(#596)](https://github.com/zowe/zlux-server-framework/pull/596) + ## 3.5.0 - Enhancement: Improved SSH connection performance restoring use of Node.js built-in diffie-hellman logic. [(#669)](https://github.com/zowe/zlux-server-framework/pull/669) - Enhancement: The app-server can now use separate certificates for inbound server TLS and outbound client TLS connections. When `zowe.certificate.keystore.clientCertificateAlias` (keyring) or both `zowe.certificate.pem.clientCertificate` and `zowe.certificate.pem.clientKey` (PEM) are defined, those are used for all outbound client connections while the main certificate is used only for serving HTTPS. When not defined, behavior is unchanged. [(#674)](https://github.com/zowe/zlux-server-framework/pull/674) diff --git a/lib/util.js b/lib/util.js index f1402937..923938bc 100644 --- a/lib/util.js +++ b/lib/util.js @@ -19,6 +19,7 @@ if (!global.COM_RS_COMMON_LOGGER) { const path = require('path'); const fs = require('fs'); +const os = require('os'); const util = require('node:util'); const Promise = require('bluebird'); const ipaddr = require('ipaddr.js'); @@ -276,49 +277,20 @@ module.exports.readFilesToArray = function(fileList, type, pass) { try { let extension = filePath.split('.').pop(); let content = fs.readFileSync(filePath); - if(extension == 'p12' || extension == 'pfx'){ - let p12Der = forge.util.decode64(content.toString('base64')); - let p12Asn1 = forge.asn1.fromDer(p12Der); - let p12; - try { - p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, pass || "password"); - } catch (e1) { - loggers.bootstrapLogger.warn("ZWED0173W", e1.message); - p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, ""); - } - const certData = p12.getBags({bagType: forge.pki.oids.certBag})[forge.pki.oids.certBag]; - const keyData = p12.getBags({bagType: forge.pki.oids.pkcs8ShroudedKeyBag})[forge.pki.oids.pkcs8ShroudedKeyBag]; - if(certData != undefined && type != 1 && type != 3){ //CRLs not currently supported by node forge - for(let i = 0; i < certData.length; i++){ - let certObj = certData[i]; - contentArray.push(Buffer.from(forge.pki.certificateToPem(certObj.cert), 'utf8')); - } - } - if(keyData != undefined && type == 1){ - for(let i = 0; i < keyData.length; i++){ - const rsaPrivateKey = forge.pki.privateKeyToAsn1(keyData[i].key); - const privateKeyInfo = forge.pki.wrapRsaPrivateKey(rsaPrivateKey); - let privateKeyPem = forge.pki.privateKeyInfoToPem(privateKeyInfo); - var buf = Buffer.from(privateKeyPem, 'utf8'); - contentArray.push(buf); - } - } - } else { - if(!content.toString().includes('-----BEGIN')){ - let der = forge.util.decode64(content.toString('base64')); - let derAsn1 = forge.asn1.fromDer(der); - if(type == 1){ - let privateKeyInfo = forge.pki.wrapRsaPrivateKey(derAsn1); - let privateKeyPem = forge.pki.privateKeyInfoToPem(privateKeyInfo); - contentArray.push(Buffer.from(privateKeyPem, 'utf8')); - } else { - let asn1Cert = forge.pki.certificateFromAsn1(derAsn1); - let pem = forge.pki.certificateToPem(asn1Cert); - contentArray.push(Buffer.from(pem, 'utf8')); - } + if(!content.toString().includes('-----BEGIN')){ + let der = forge.util.decode64(content.toString('base64')); + let derAsn1 = forge.asn1.fromDer(der); + if(type == 1){ + let privateKeyInfo = forge.pki.wrapRsaPrivateKey(derAsn1); + let privateKeyPem = forge.pki.privateKeyInfoToPem(privateKeyInfo); + contentArray.push(Buffer.from(privateKeyPem, 'utf8')); } else { - contentArray.push(content); + let asn1Cert = forge.pki.certificateFromAsn1(derAsn1); + let pem = forge.pki.certificateToPem(asn1Cert); + contentArray.push(Buffer.from(pem, 'utf8')); } + } else { + contentArray.push(content); } } catch (e) { loggers.bootstrapLogger.warn("ZWED0052W", filePath, e.message); //loggers.bootstrapLogger.warn('Error when reading file='+filePath+'. Error='+e.message); @@ -593,6 +565,27 @@ function getComponentConfig(zoweConfig) { return zoweConfig.components['app-server']; } module.exports.getComponentConfig = getComponentConfig; + +// O_BINARY (0x8000) is a z/OS USS-specific open() flag that suppresses +// EBCDIC-to-ASCII auto-conversion at the kernel level for this file descriptor, +// regardless of _BPXK_AUTOCVT setting or the file's CCSID tag. Without this, +// untagged .p12 files on z/OS are silently corrupted before Node.js reads them. +function readFileAsBinaryString(filePath) { + if (os.platform() !== 'os390') { + return fs.readFileSync(filePath, 'binary'); + } + const O_BINARY = 0x8000; + const fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_BINARY); + try { + const size = fs.fstatSync(fd).size; + const buffer = Buffer.alloc(size); + fs.readSync(fd, buffer, 0, size, 0); + return buffer.toString('binary'); + } finally { + fs.closeSync(fd); + } +} +module.exports.readFileAsBinaryString = readFileAsBinaryString; /* This program and the accompanying materials are made available under the terms of the Eclipse Public License v2.0 which accompanies diff --git a/lib/webserver.js b/lib/webserver.js index fbc5b80d..a79d404f 100644 --- a/lib/webserver.js +++ b/lib/webserver.js @@ -29,6 +29,8 @@ const contentLogger = util.loggers.contentLogger; const childLogger = util.loggers.childLogger; const networkLogger = util.loggers.network; +const forge = require('node-forge'); + const os = require('os'); let keyring_js; try { @@ -130,8 +132,76 @@ function splitCryptoLocationsByType(locations) { return locationsByType; } +function loadPkcs12(location, pass) { + const p12Content = util.readFileAsBinaryString(location); + const p12Asn1 = forge.asn1.fromDer(p12Content); + const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, pass); + return p12; +} + +function getP12Content(p12, type, alias) { + //An array is expected, but just one object should be in it if we're looking for key/cert instead of CA. + let value = []; + + for (let i = 0; i < p12.safeContents.length; i++) { + let safeContents = p12.safeContents[i]; + bootstrapLogger.debug(`iterating through pkcs12 safe contents ${i}`); + for (let j = 0; j < safeContents.safeBags.length; j++) { + bootstrapLogger.debug(`iterating through pkcs12 bag ${j}`); + let bag = safeContents.safeBags[j]; + if (type == CRYPTO_CONTENT_CERT && bag.type === forge.pki.oids.certBag) { + if (bag.attributes.friendlyName == alias) { + value.push(forge.pki.certificateToPem(bag.cert)); + bootstrapLogger.debug(`Found pkcs12 certificate`); + return value; + } + } else if (type == CRYPTO_CONTENT_KEY && bag.type === forge.pki.oids.keyBag) { + if (bag.attributes.friendlyName == alias) { + value.push(forge.pki.privateKeyToPem(bag.key)); + bootstrapLogger.debug(`Found pkcs12 key`); + return value; + } + } else if (type == CRYPTO_CONTENT_KEY && bag.type === forge.pki.oids.pkcs8ShroudedKeyBag) { + if (bag.attributes.friendlyName == alias) { + value.push(forge.pki.privateKeyToPem(bag.key)); + bootstrapLogger.debug(`Found pkcs12 key`); + return value; + } + } else if (type == CRYPTO_CONTENT_CA && bag.type === forge.pki.oids.certBag && bag.cert?.extensions) { + let isCa = false; + for (let k = 0; k < bag.cert.extensions.length; k++) { + let extension = bag.cert.extensions[k]; + bootstrapLogger.debug(`Potential CA extension ${k} = `,extension); + //2.5.29.19 + if (extension.id == forge.pki.oids.basicConstraints) { + isCa = extension.cA; + break; + } + /* + // 2.5.29.15 has 'keyCertSign: true' on a CA, i guess? + if (extension.id == forge.pki.oids.keyUsage) { + isCa = extension.keyCertSign; + } + */ + } + if (isCa) { + value.push(forge.pki.certificateToPem(bag.cert)); + bootstrapLogger.debug(`Found CA with name `+bag.attributes.friendlyName); + } + } else if (type == CRYPTO_CONTENT_CRL && bag.type === forge.pki.oids.crlBag) { + bootstrapLogger.warn(`Certificate Revocation List requested and found, but unhandled`); + return []; + } else { + bootstrapLogger.debug(`Ignoring PKCS12 object type=${bag.type}`); + } + } + } + + return value; +} + // safkeyring:// -function loadPem(locations, type, keyrings, pass) { +function loadPem(locations, type, keyringCache, p12Cache, pass, alias) { const locationsByType = splitCryptoLocationsByType(locations); let content = []; const types = Object.keys(locationsByType); @@ -152,13 +222,13 @@ function loadPem(locations, type, keyrings, pass) { const {userId, keyringName, label} = parseSafKeyringAddress(safRingAddress); if (userId && keyringName && label) { const cachedKey = 'safkeyring://'+safRingAddress; - let keyringData = keyrings[cachedKey]; + let keyringData = keyringCache[cachedKey]; const attribute = getAttributeNameForCryptoType('safkeyring', type); try { if (!keyringData) { bootstrapLogger.debug(`Cache not found for ${cachedKey}`); keyringData = keyring_js.getPemEncodedData(userId, keyringName, label); - keyrings[cachedKey] = keyringData; + keyringCache[cachedKey] = keyringData; } if (keyringData) { if (keyringData[attribute]) { @@ -185,51 +255,58 @@ function loadPem(locations, type, keyrings, pass) { //Cannot load SAF keyring due to missing keyring_js library'); bootstrapLogger.warn('ZWED0150E'); } + const files = locationsByType['file']; - if (files) { - //workaround for a bug outside zlux: seems that some yaml files may come in with strings with trailing ',', just strip it. - content = util.readFilesToArray(files.map(file => file.charAt(file.length-1)==',' ? file.substring(0, file.length-1) : file), - type, pass).concat(content); + if (files && (files.length > 0)) { + const pemFiles = files.filter(name => !name.endsWith('.p12') && !name.endsWith('.pfx')); + const p12Files = files.filter(name => name.endsWith('.p12') || name.endsWith('.pfx')); + for (let i = 0; i < p12Files.length; i++) { + let p12 = p12Cache[p12Files[i]]; + if (!p12) { + p12 = loadPkcs12(p12Files[i], pass); + p12Cache[p12Files[i]] = p12; + } + + content = getP12Content(p12, type, alias); + + // It does not make sense to load multiple cert/keys, does it? + if ((content.length >= 1) && ((type == CRYPTO_CONTENT_CERT) || (type == CRYPTO_CONTENT_KEY))) { break; } + } + if (pemFiles && pemFiles.length > 0) { + bootstrapLogger.warn(`PEM is insecure storage for TLS key material, consider using PKCS12 or SAF keyrings instead.`); + //workaround for a bug outside zlux: seems that some yaml files may come in with strings with trailing ',', just strip it. + content = util.readFilesToArray(pemFiles.map(file => file.charAt(file.length-1)==',' ? file.substring(0, file.length-1) : file), + type, pass).concat(content); + } } - return {content, keyrings}; + return {content, keyringCache, p12Cache}; } -function readTlsOptionsFromConfig(nodeConfig, httpsOptions, pass) { +function readTlsOptionsFromConfig(nodeConfig, httpsOptions, keystorePass, truststorePass, alias, clientAlias) { //in case keys and certs can be read from the same keyring, store them here for later retrieval - let keyrings = {}; - if (nodeConfig.https.pfx) { - try { - httpsOptions.pfx = fs.readFileSync(nodeConfig.https.pfx); - bootstrapLogger.info('ZWED0071I', nodeConfig.https.pfx); //bootstrapLogger.info('Using PFX: '+ nodeConfig.https.pfx); - } catch (e) { - bootstrapLogger.warn('ZWED0070W', e.message); //bootstrapLogger.warn('Error when reading PFX. Server cannot continue. Error=' - //+ e.message); - process.exit(constants.EXIT_PFX_READ); - throw e; - } - } else { - if (nodeConfig.https.certificates) { - httpsOptions.cert = loadPem(nodeConfig.https.certificates, CRYPTO_CONTENT_CERT, keyrings, pass).content; - bootstrapLogger.info('ZWED0072I', nodeConfig.https.certificates); //bootstrapLogger.info('Using Certificate: ' + nodeConfig.https.certificates); - } - if (nodeConfig.https.keys) { - httpsOptions.key = loadPem(nodeConfig.https.keys, CRYPTO_CONTENT_KEY, keyrings, pass).content; - } + let keyringCache = {}; + let p12Cache = {}; + if (nodeConfig.https.certificates) { + httpsOptions.cert = loadPem(nodeConfig.https.certificates, CRYPTO_CONTENT_CERT, keyringCache, p12Cache, keystorePass, alias).content; + bootstrapLogger.info('ZWED0072I', nodeConfig.https.certificates); //bootstrapLogger.info('Using Certificate: ' + nodeConfig.https.certificates); + } + if (nodeConfig.https.keys) { + httpsOptions.key = loadPem(nodeConfig.https.keys, CRYPTO_CONTENT_KEY, keyringCache, p12Cache, keystorePass, alias).content; } // Load optional client-specific certificates. When present, these will be used for // outbound client TLS connections instead of the server certificates above. if (nodeConfig.https.clientCertificates) { - httpsOptions.clientCert = loadPem(nodeConfig.https.clientCertificates, CRYPTO_CONTENT_CERT, keyrings, pass).content; + httpsOptions.clientCert = loadPem(nodeConfig.https.clientCertificates, CRYPTO_CONTENT_CERT, keyringCache, p12Cache, keystorePass, clientAlias).content; bootstrapLogger.info('ZWED0304I', nodeConfig.https.clientCertificates); } if (nodeConfig.https.clientKeys) { - httpsOptions.clientKey = loadPem(nodeConfig.https.clientKeys, CRYPTO_CONTENT_KEY, keyrings, pass).content; + httpsOptions.clientKey = loadPem(nodeConfig.https.clientKeys, CRYPTO_CONTENT_KEY, keyringCache, p12Cache, keystorePass, clientAlias).content; } if (nodeConfig.https.certificateAuthorities) { - httpsOptions.ca = loadPem(nodeConfig.https.certificateAuthorities, CRYPTO_CONTENT_CA, keyrings, pass).content; + httpsOptions.ca = loadPem(nodeConfig.https.certificateAuthorities, CRYPTO_CONTENT_CA, keyringCache, p12Cache, truststorePass).content; } if (nodeConfig.https.certificateRevocationLists) { - httpsOptions.crl = loadPem(nodeConfig.https.certificateRevocationLists, CRYPTO_CONTENT_CRL, keyrings, pass).content; + httpsOptions.crl = loadPem(nodeConfig.https.certificateRevocationLists, CRYPTO_CONTENT_CRL, keyringCache, p12Cache, keystorePass).content; } } @@ -420,10 +497,14 @@ WebServer.prototype = { this.httpsOptions.enableTrace = true; } bootstrapLogger.debug('TLS trace:', this.httpsOptions.enableTrace ? 'enabled' : 'disabled'); - if (util.isServerHttps(zoweConfig) || !(util.isClientAttls(zoweConfig))) { - readTlsOptionsFromConfig(nodeConfig, this.httpsOptions, zoweConfig.zowe?.certificate?.keystore?.password); + readTlsOptionsFromConfig(nodeConfig, this.httpsOptions, + zoweConfig.zowe?.certificate?.keystore?.password, + zoweConfig.zowe?.certificate?.truststore?.password, + zoweConfig.zowe?.certificate?.keystore?.alias, + zoweConfig.zowe?.certificate?.keystore?.certificateAlias + ); } // Split into server and client TLS options when distinct client certificates are configured. diff --git a/utils/certificateChecker.js b/utils/certificateChecker.js index 7e3e6696..744efe5f 100644 --- a/utils/certificateChecker.js +++ b/utils/certificateChecker.js @@ -27,6 +27,7 @@ try { logger.warn('Could not load zcrypto library, SAF keyrings will be unavailable'); } const forge = require('node-forge'); +const { readFileAsBinaryString } = require('../lib/util'); const argParser = require('./argumentParser'); @@ -94,7 +95,7 @@ if (userInput.type == 'JCERACFKS') { cert = forge.pki.certificateFromPem(pem); } else if (userInput.type == 'PKCS12') { logger.debug('Checking PKCS12 for alias '+userInput.alias); - const p12Content = fs.readFileSync(userInput.certificate, 'binary'); + const p12Content = readFileAsBinaryString(userInput.certificate); const p12Asn1 = forge.asn1.fromDer(p12Content); const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, process.env.ZWE_zowe_certificate_keystore_password); for (let i = 0; i < p12.safeContents.length; i++) {