Skip to content
Draft
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
75 changes: 34 additions & 41 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
149 changes: 115 additions & 34 deletions lib/webserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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]) {
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion utils/certificateChecker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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++) {
Expand Down
Loading