Files
mq-container/internal/tls/tls.go
Alec Painter e0c3b36b61 [ci skip]: Update v9.3.1 branch for MQ 9.3.1.0-r2 (#350)
* Update MQ version to 9.3.1.0-r2

* First part of the changes for SSLKEYR  (#328)

* Squashed all commits

* Addressed review comments

* Fix JMS test build issue (#340)

* Fix JMS test build issue

* Remove ciphername where not required

* Fix issue1766 and add test case (#336)

* Fix issue1766 and add test case

* Address review comments

* Updated copyright year

* Resolve merge conflicts

* Updating changelog (#346)

* Updating changelog

* Updating changelog

* updated go-version & ubi
2022-11-21 15:02:25 +00:00

643 lines
21 KiB
Go

/*
© Copyright IBM Corporation 2019, 2022
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tls
import (
"bufio"
"fmt"
"io/ioutil"
pwr "math/rand"
"os"
"path/filepath"
"strings"
"time"
"crypto/rand"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
pkcs "software.sslmate.com/src/go-pkcs12"
"github.com/ibm-messaging/mq-container/internal/keystore"
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
// cmsKeystoreName is the name of the CMS Keystore
const cmsKeystoreName = "key.kdb"
// p12TruststoreName is the name of the PKCS#12 Truststore
const p12TruststoreName = "trust.p12"
// keystoreDirDefault is the location for the default CMS Keystore & PKCS#12 Truststore
const keystoreDirDefault = "/run/runmqserver/tls/"
// keystoreDirHA is the location for the HA CMS Keystore
const keystoreDirHA = "/run/runmqserver/ha/tls/"
// keyDirDefault is the location of the default keys to import
const keyDirDefault = "/etc/mqm/pki/keys"
// keyDirHA is the location of the HA keys to import
const keyDirHA = "/etc/mqm/ha/pki/keys"
// trustDirDefault is the location of the trust certificates to import
const trustDirDefault = "/etc/mqm/pki/trust"
type KeyStoreData struct {
Keystore *keystore.KeyStore
Password string
TrustedCerts []*pem.Block
KnownFingerPrints []string
KeyLabels []string
}
type P12KeyFiles struct {
Keystores []string
Password string
}
type TLSStore struct {
Keystore KeyStoreData
Truststore KeyStoreData
}
func configureTLSKeystores(keystoreDir, keyDir, trustDir string, p12TruststoreRequired bool, nativeTLSHA bool) (string, KeyStoreData, KeyStoreData, error) {
var keyLabel string
// Create the CMS Keystore & PKCS#12 Truststore (if required)
tlsStore, err := generateAllKeystores(keystoreDir, p12TruststoreRequired, nativeTLSHA)
if err != nil {
return "", tlsStore.Keystore, tlsStore.Truststore, err
}
if tlsStore.Keystore.Keystore != nil {
// Process all keys - add them to the CMS KeyStore
keyLabel, err = processKeys(&tlsStore, keystoreDir, keyDir)
if err != nil {
return "", tlsStore.Keystore, tlsStore.Truststore, err
}
}
// Process all trust certificates - add them to the CMS KeyStore & PKCS#12 Truststore (if required)
err = processTrustCertificates(&tlsStore, trustDir)
if err != nil {
return "", tlsStore.Keystore, tlsStore.Truststore, err
}
return keyLabel, tlsStore.Keystore, tlsStore.Truststore, err
}
// ConfigureDefaultTLSKeystores configures the CMS Keystore & PKCS#12 Truststore
func ConfigureDefaultTLSKeystores() (string, KeyStoreData, KeyStoreData, error) {
return configureTLSKeystores(keystoreDirDefault, keyDirDefault, trustDirDefault, true, false)
}
// ConfigureHATLSKeystore configures the CMS Keystore & PKCS#12 Truststore
func ConfigureHATLSKeystore() (string, KeyStoreData, KeyStoreData, error) {
// *.crt files mounted to the HA TLS dir keyDirHA will be processed as trusted in the CMS keystore
return configureTLSKeystores(keystoreDirHA, keyDirHA, keyDirHA, false, true)
}
// ConfigureTLS configures TLS for the queue manager
func ConfigureTLS(keyLabel string, cmsKeystore KeyStoreData, devMode bool, log *logger.Logger) error {
const mqsc string = "/etc/mqm/15-tls.mqsc"
const mqscTemplate string = mqsc + ".tpl"
sslKeyRing := ""
// Don't set SSLKEYR if no keys or crts are not supplied
// Key label will be blank if no certs were added during processing keys and certs.
if cmsKeystore.Keystore != nil {
certList, _ := cmsKeystore.Keystore.ListAllCertificates()
if len(certList) > 0 {
sslKeyRing = strings.TrimSuffix(cmsKeystore.Keystore.Filename, ".kdb")
}
}
err := mqtemplate.ProcessTemplateFile(mqscTemplate, mqsc, map[string]string{
"SSLKeyR": sslKeyRing,
"CertificateLabel": keyLabel,
}, log)
if err != nil {
return err
}
if devMode && keyLabel != "" {
err = configureTLSDev(log)
if err != nil {
return err
}
}
return nil
}
// configureTLSDev configures TLS for the developer defaults
func configureTLSDev(log *logger.Logger) error {
const mqsc string = "/etc/mqm/20-dev-tls.mqsc"
const mqscTemplate string = mqsc + ".tpl"
if os.Getenv("MQ_DEV") == "true" {
err := mqtemplate.ProcessTemplateFile(mqscTemplate, mqsc, map[string]string{}, log)
if err != nil {
return err
}
} else {
_, err := os.Stat(mqsc)
if !os.IsNotExist(err) {
err = os.Remove(mqsc)
if err != nil {
return fmt.Errorf("Failed to remove file %s: %v", mqsc, err)
}
}
}
return nil
}
// generateAllKeystores creates the CMS Keystore & PKCS#12 Truststore (if required)
func generateAllKeystores(keystoreDir string, p12TruststoreRequired bool, nativeTLSHA bool) (TLSStore, error) {
var cmsKeystore, p12Truststore KeyStoreData
// Generate a pasword for use with both the CMS Keystore & PKCS#12 Truststore
pw := generateRandomPassword()
cmsKeystore.Password = pw
p12Truststore.Password = pw
// Create the Keystore directory - if it does not already exist
// #nosec G301 - write group permissions are required
err := os.MkdirAll(keystoreDir, 0770)
if err != nil {
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create Keystore directory: %v", err)
}
// Search the default keys directory for any keys/certs.
keysDirectory := keyDirDefault
// Change to default native HA TLS directory if we are configuring nativeHA
if nativeTLSHA {
keysDirectory = keyDirHA
}
// Create the CMS Keystore if we have been provided keys and certificates
if haveKeysAndCerts(keysDirectory) || haveKeysAndCerts(trustDirDefault) {
cmsKeystore.Keystore = keystore.NewCMSKeyStore(filepath.Join(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
err = cmsKeystore.Keystore.Create()
if err != nil {
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create CMS Keystore: %v", err)
}
}
// Create the PKCS#12 Truststore (if required)
if p12TruststoreRequired {
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(filepath.Join(keystoreDir, p12TruststoreName), p12Truststore.Password)
err = p12Truststore.Keystore.Create()
if err != nil {
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create PKCS#12 Truststore: %v", err)
}
}
return TLSStore{cmsKeystore, p12Truststore}, nil
}
// processKeys processes all keys - adding them to the CMS KeyStore
func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string, error) {
// Key label - will be set to the label of the first set of keys
keyLabel := ""
// Process all keys
keyList, err := ioutil.ReadDir(keyDir)
if err == nil && len(keyList) > 0 {
// Process each set of keys - each set should contain files: *.key & *.crt
for _, keySet := range keyList {
keys, _ := ioutil.ReadDir(filepath.Join(keyDir, keySet.Name()))
// Ensure the label of the set of keys does not match the name of the PKCS#12 Truststore
if keySet.Name() == p12TruststoreName[0:len(p12TruststoreName)-len(filepath.Ext(p12TruststoreName))] {
return "", fmt.Errorf("Key label cannot be set to the Truststore name: %v", keySet.Name())
}
// Process private key (*.key)
privateKey, keyPrefix, err := processPrivateKey(keyDir, keySet.Name(), keys)
if err != nil {
return "", err
}
// If private key does not exist - skip this set of keys
if privateKey == nil {
continue
}
// Process certificates (*.crt) - public certificate & optional CA certificate
publicCertificate, caCertificate, err := processCertificates(keyDir, keySet.Name(), keyPrefix, keys, &tlsStore.Keystore, &tlsStore.Truststore)
if err != nil {
return "", err
}
// Create a new PKCS#12 Keystore - containing private key, public certificate & optional CA certificate
file, err := pkcs.Encode(rand.Reader, privateKey, publicCertificate, caCertificate, tlsStore.Keystore.Password)
if err != nil {
return "", fmt.Errorf("Failed to encode PKCS#12 Keystore %s: %v", keySet.Name()+".p12", err)
}
err = ioutil.WriteFile(filepath.Join(keystoreDir, keySet.Name()+".p12"), file, 0644)
if err != nil {
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", filepath.Join(keystoreDir, keySet.Name()+".p12"), err)
}
// Import the new PKCS#12 Keystore into the CMS Keystore
err = tlsStore.Keystore.Keystore.Import(filepath.Join(keystoreDir, keySet.Name()+".p12"), tlsStore.Keystore.Password)
if err != nil {
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", filepath.Join(keystoreDir, keySet.Name()+".p12"), err)
}
// Relabel the certificate in the CMS Keystore
err = relabelCertificate(keySet.Name(), &tlsStore.Keystore)
if err != nil {
return "", err
}
// Set key label - for first set of keys only
if keyLabel == "" {
keyLabel = keySet.Name()
}
}
}
return keyLabel, nil
}
// processTrustCertificates processes all trust certificates - adding them to the CMS KeyStore & PKCS#12 Truststore (if required)
func processTrustCertificates(tlsStore *TLSStore, trustDir string) error {
// Process all trust certiifcates
trustList, err := ioutil.ReadDir(trustDir)
if err == nil && len(trustList) > 0 {
// Process each set of keys
for _, trustSet := range trustList {
keys, _ := ioutil.ReadDir(filepath.Join(trustDir, trustSet.Name()))
for _, key := range keys {
if strings.HasSuffix(key.Name(), ".crt") {
// #nosec G304 - filename variable is derived from contents of 'trustDir' which is a defined constant
file, err := ioutil.ReadFile(filepath.Join(trustDir, trustSet.Name(), key.Name()))
if err != nil {
return fmt.Errorf("Failed to read file %s: %v", filepath.Join(trustDir, trustSet.Name(), key.Name()), err)
}
for string(file) != "" {
var block *pem.Block
block, file = pem.Decode(file)
if block == nil {
break
}
// Add to known certificates for the CMS Keystore
err = addToKnownCertificates(block, &tlsStore.Keystore, true)
if err != nil {
return fmt.Errorf("Failed to add to know certificates for CMS Keystore")
}
if tlsStore.Truststore.Keystore != nil {
// Add to known certificates for the PKCS#12 Truststore
err = addToKnownCertificates(block, &tlsStore.Truststore, true)
if err != nil {
return fmt.Errorf("Failed to add to know certificates for PKCS#12 Truststore")
}
}
}
}
}
}
}
// Add all trust certificates to PKCS#12 Truststore (if required)
if tlsStore.Truststore.Keystore != nil && len(tlsStore.Truststore.TrustedCerts) > 0 {
err = addCertificatesToTruststore(&tlsStore.Truststore)
if err != nil {
return err
}
}
// Add all trust certificates to CMS Keystore
if len(tlsStore.Keystore.TrustedCerts) > 0 {
err = addCertificatesToCMSKeystore(&tlsStore.Keystore)
if err != nil {
return err
}
}
return nil
}
// processPrivateKey processes the private key (*.key) from a set of keys
func processPrivateKey(keyDir string, keySetName string, keys []os.FileInfo) (interface{}, string, error) {
var privateKey interface{}
keyPrefix := ""
for _, key := range keys {
if strings.HasSuffix(key.Name(), ".key") {
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
file, err := ioutil.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
if err != nil {
return nil, "", fmt.Errorf("Failed to read private key %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
}
block, _ := pem.Decode(file)
if block == nil {
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", filepath.Join(keyDir, keySetName, key.Name()))
}
// Check if the private key is PKCS1
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
// Check if the private key is PKCS8
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
}
}
keyPrefix = key.Name()[0 : len(key.Name())-len(filepath.Ext(key.Name()))]
}
}
return privateKey, keyPrefix, nil
}
// processCertificates processes the certificates (*.crt) from a set of keys
func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.FileInfo, cmsKeystore, p12Truststore *KeyStoreData) (*x509.Certificate, []*x509.Certificate, error) {
var publicCertificate *x509.Certificate
var caCertificate []*x509.Certificate
for _, key := range keys {
if strings.HasPrefix(key.Name(), keyPrefix) && strings.HasSuffix(key.Name(), ".crt") {
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
file, err := ioutil.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
if err != nil {
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
}
block, _ := pem.Decode(file)
if block == nil {
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", filepath.Join(keyDir, keySetName, key.Name()))
}
publicCertificate, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
}
// Add to known certificates for the CMS Keystore
err = addToKnownCertificates(block, cmsKeystore, false)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to know certificates for CMS Keystore")
}
} else if strings.HasSuffix(key.Name(), ".crt") {
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
file, err := ioutil.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
if err != nil {
return nil, nil, fmt.Errorf("Failed to read CA certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
}
for string(file) != "" {
var block *pem.Block
block, file = pem.Decode(file)
if block == nil {
break
}
// Add to known certificates for the CMS Keystore
err = addToKnownCertificates(block, cmsKeystore, false)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to know certificates for CMS Keystore")
}
if p12Truststore.Keystore != nil {
// Add to known certificates for the PKCS#12 Truststore
err = addToKnownCertificates(block, p12Truststore, true)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to know certificates for PKCS#12 Truststore")
}
}
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
}
caCertificate = append(caCertificate, certificate)
}
}
}
return publicCertificate, caCertificate, nil
}
// relabelCertificate sets a new label for a certificate in the CMS Keystore
func relabelCertificate(newLabel string, cmsKeystore *KeyStoreData) error {
allLabels, err := cmsKeystore.Keystore.GetCertificateLabels()
if err != nil {
return fmt.Errorf("Failed to get list of all certificate labels from CMS Keystore: %v", err)
}
relabelled := false
for _, label := range allLabels {
found := false
for _, keyLabel := range cmsKeystore.KeyLabels {
if strings.Trim(label, "\"") == keyLabel {
found = true
break
}
}
if !found {
err = cmsKeystore.Keystore.RenameCertificate(strings.Trim(label, "\""), newLabel)
if err != nil {
return err
}
relabelled = true
cmsKeystore.KeyLabels = append(cmsKeystore.KeyLabels, newLabel)
break
}
}
if !relabelled {
return fmt.Errorf("Failed to relabel certificate for %s in CMS keystore", newLabel)
}
return nil
}
// addCertificatesToTruststore adds trust certificates to the PKCS#12 Truststore
func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
temporaryPemFile := filepath.Join("/tmp", "trust.pem")
_, err := os.Stat(temporaryPemFile)
if err == nil {
err = os.Remove(temporaryPemFile)
if err != nil {
return fmt.Errorf("Failed to remove file %v: %v", temporaryPemFile, err)
}
}
err = writeCertificatesToFile(temporaryPemFile, p12Truststore.TrustedCerts)
if err != nil {
return err
}
err = p12Truststore.Keystore.AddNoLabel(temporaryPemFile)
if err != nil {
return fmt.Errorf("Failed to add certificates to PKCS#12 Truststore: %v", err)
}
// Relabel all certiifcates
allCertificates, err := p12Truststore.Keystore.ListAllCertificates()
if err != nil || len(allCertificates) <= 0 {
return fmt.Errorf("Failed to get any certificates from PKCS#12 Truststore: %v", err)
}
for i, certificate := range allCertificates {
certificate = strings.Trim(certificate, "\"")
certificate = strings.TrimSpace(certificate)
newLabel := fmt.Sprintf("Trust%d", i)
err = p12Truststore.Keystore.RenameCertificate(certificate, newLabel)
if err != nil || len(allCertificates) <= 0 {
return fmt.Errorf("Failed to rename certificate %s to %s in PKCS#12 Truststore: %v", certificate, newLabel, err)
}
}
return nil
}
// addCertificatesToCMSKeystore adds trust certificates to the CMS keystore
func addCertificatesToCMSKeystore(cmsKeystore *KeyStoreData) error {
temporaryPemFile := filepath.Join("/tmp", "cmsTrust.pem")
_, err := os.Stat(temporaryPemFile)
if err == nil {
err = os.Remove(temporaryPemFile)
if err != nil {
return fmt.Errorf("Failed to remove file %v: %v", temporaryPemFile, err)
}
}
err = writeCertificatesToFile(temporaryPemFile, cmsKeystore.TrustedCerts)
if err != nil {
return err
}
err = cmsKeystore.Keystore.AddNoLabel(temporaryPemFile)
if err != nil {
return fmt.Errorf("Failed to add certificates to CMS keystore: %v", err)
}
return nil
}
// generateRandomPassword generates a random 12 character password from the characters a-z, A-Z, 0-9
func generateRandomPassword() string {
pwr.Seed(time.Now().Unix())
validChars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
validcharArray := []byte(validChars)
password := ""
for i := 0; i < 12; i++ {
password = password + string(validcharArray[pwr.Intn(len(validcharArray))])
}
return password
}
// addToKnownCertificates adds to the list of known certificates for a Keystore
func addToKnownCertificates(block *pem.Block, keyData *KeyStoreData, addToKeystore bool) error {
sha512str, err := getCertificateFingerprint(block)
if err != nil {
return err
}
known := false
for _, fingerprint := range keyData.KnownFingerPrints {
if fingerprint == sha512str {
known = true
break
}
}
if !known {
if addToKeystore {
keyData.TrustedCerts = append(keyData.TrustedCerts, block)
}
keyData.KnownFingerPrints = append(keyData.KnownFingerPrints, sha512str)
}
return nil
}
// getCertificateFingerprint returns a fingerprint for a certificate
func getCertificateFingerprint(block *pem.Block) (string, error) {
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", fmt.Errorf("Failed to parse x509 certificate: %v", err)
}
sha512Sum := sha512.Sum512(certificate.Raw)
sha512str := string(sha512Sum[:])
return sha512str, nil
}
// writeCertificatesToFile writes a list of certificates to a file
func writeCertificatesToFile(file string, certificates []*pem.Block) error {
f, err := os.Create(file)
if err != nil {
return fmt.Errorf("Failed to create file %s: %v", file, err)
}
defer f.Close()
w := bufio.NewWriter(f)
for i, c := range certificates {
err := pem.Encode(w, c)
if err != nil {
return fmt.Errorf("Failed to encode certificate number %d: %v", i, err)
}
err = w.Flush()
if err != nil {
return fmt.Errorf("Failed to write certificate to file %s: %v", file, err)
}
}
return nil
}
// Search the specified directory for .key and .crt files.
// Return true if at least one .key or .crt file is found else false
func haveKeysAndCerts(keyDir string) bool {
fileList, err := os.ReadDir(keyDir)
if err == nil && len(fileList) > 0 {
for _, fileInfo := range fileList {
// Keys and certs will be supplied in an user defined subdirectory.
// Do a listing of the subdirectory and then search for .key and .cert files
keys, _ := ioutil.ReadDir(filepath.Join(keyDir, fileInfo.Name()))
for _, key := range keys {
if strings.Contains(key.Name(), ".key") || strings.Contains(key.Name(), ".crt") {
// We found at least one key/crt file.
return true
}
}
}
}
return false
}