From 234d68f48d7f81f5cc9ec799e8a2a17327613b03 Mon Sep 17 00:00:00 2001 From: Tom Weininger Date: Tue, 17 Sep 2024 10:12:40 +0000 Subject: [PATCH] Add own implementation of PKCS#8 key encryption This is inspired by [1] (Apache-2.0 license) [1]: https://github.com/smallstep/crypto --- pkg/octavia/amphora_certs.go | 7 +- pkg/octavia/pem_decrypt_fips.go | 152 ------------------------------- pkg/octavia/pkcs8_aes.go | 154 ++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 158 deletions(-) delete mode 100644 pkg/octavia/pem_decrypt_fips.go create mode 100644 pkg/octavia/pkcs8_aes.go diff --git a/pkg/octavia/amphora_certs.go b/pkg/octavia/amphora_certs.go index f73a42b9..56cf76d8 100644 --- a/pkg/octavia/amphora_certs.go +++ b/pkg/octavia/amphora_certs.go @@ -60,12 +60,7 @@ func generateKey(passphrase []byte) (*rsa.PrivateKey, []byte, error) { var pemBlock *pem.Block if passphrase != nil { - pemBlock, err = EncryptPEMBlock( - rand.Reader, - "PRIVATE KEY", - pkcs8Key, - passphrase, - PEMCipherAES128) + pemBlock, err = EncryptPrivateKey(pkcs8Key, passphrase) if err != nil { err = fmt.Errorf("Error encrypting private key: %w", err) return priv, nil, err diff --git a/pkg/octavia/pem_decrypt_fips.go b/pkg/octavia/pem_decrypt_fips.go deleted file mode 100644 index a776cc83..00000000 --- a/pkg/octavia/pem_decrypt_fips.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This code was derived from Golang's -// src/crypto/x509/pem_decrypt.go. It has been modified in order to -// become compliant with FIPS requirements. The use of MD5 has been -// replaced with SHA256. - -package octavia - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/des" - "crypto/sha256" - "encoding/hex" - "encoding/pem" - "errors" - "io" -) - -type PEMCipher int - -// Possible values for the EncryptPEMBlock encryption algorithm. -const ( - _ PEMCipher = iota - PEMCipherDES - PEMCipher3DES - PEMCipherAES128 - PEMCipherAES192 - PEMCipherAES256 -) - -// rfc1423Algo holds a method for enciphering a PEM block. -type rfc1423Algo struct { - cipher PEMCipher - name string - cipherFunc func(key []byte) (cipher.Block, error) - keySize int - blockSize int -} - -// rfc1423Algos holds a slice of the possible ways to encrypt a PEM -// block. The ivSize numbers were taken from the OpenSSL source. -var rfc1423Algos = []rfc1423Algo{{ - cipher: PEMCipherDES, - name: "DES-CBC", - cipherFunc: des.NewCipher, - keySize: 8, - blockSize: des.BlockSize, -}, { - cipher: PEMCipher3DES, - name: "DES-EDE3-CBC", - cipherFunc: des.NewTripleDESCipher, - keySize: 24, - blockSize: des.BlockSize, -}, { - cipher: PEMCipherAES128, - name: "AES-128-CBC", - cipherFunc: aes.NewCipher, - keySize: 16, - blockSize: aes.BlockSize, -}, { - cipher: PEMCipherAES192, - name: "AES-192-CBC", - cipherFunc: aes.NewCipher, - keySize: 24, - blockSize: aes.BlockSize, -}, { - cipher: PEMCipherAES256, - name: "AES-256-CBC", - cipherFunc: aes.NewCipher, - keySize: 32, - blockSize: aes.BlockSize, -}, -} - -// deriveKey uses a key derivation function to stretch the password into a key -// with the number of bits our cipher requires. This algorithm was derived from -// the OpenSSL source. -func (c rfc1423Algo) deriveKey(password, salt []byte) []byte { - hash := sha256.New() - out := make([]byte, c.keySize) - var digest []byte - - for i := 0; i < len(out); i += len(digest) { - hash.Reset() - hash.Write(digest) - hash.Write(password) - hash.Write(salt) - digest = hash.Sum(digest[:0]) - copy(out[i:], digest) - } - return out -} - -// EncryptPEMBlock returns a PEM block of the specified type holding the -// given DER encoded data encrypted with the specified algorithm and -// password according to RFC 1423. -// -// Deprecated: Legacy PEM encryption as specified in RFC 1423 is insecure by -// design. Since it does not authenticate the ciphertext, it is vulnerable to -// padding oracle attacks that can let an attacker recover the plaintext. -func EncryptPEMBlock(rand io.Reader, blockType string, data, password []byte, alg PEMCipher) (*pem.Block, error) { - ciph := cipherByKey(alg) - if ciph == nil { - return nil, errors.New("x509: unknown encryption mode") - } - iv := make([]byte, ciph.blockSize) - if _, err := io.ReadFull(rand, iv); err != nil { - return nil, errors.New("x509: cannot generate IV: " + err.Error()) - } - // The salt is the first 8 bytes of the initialization vector, - // matching the key derivation in DecryptPEMBlock. - key := ciph.deriveKey(password, iv[:8]) - block, err := ciph.cipherFunc(key) - if err != nil { - return nil, err - } - enc := cipher.NewCBCEncrypter(block, iv) - pad := ciph.blockSize - len(data)%ciph.blockSize - encrypted := make([]byte, len(data), len(data)+pad) - // We could save this copy by encrypting all the whole blocks in - // the data separately, but it doesn't seem worth the additional - // code. - copy(encrypted, data) - // See RFC 1423, Section 1.1. - for i := 0; i < pad; i++ { - encrypted = append(encrypted, byte(pad)) - } - enc.CryptBlocks(encrypted, encrypted) - - return &pem.Block{ - Type: blockType, - Headers: map[string]string{ - "Proc-Type": "4,ENCRYPTED", - "DEK-Info": ciph.name + "," + hex.EncodeToString(iv), - }, - Bytes: encrypted, - }, nil -} - -func cipherByKey(key PEMCipher) *rfc1423Algo { - for i := range rfc1423Algos { - alg := &rfc1423Algos[i] - if alg.cipher == key { - return alg - } - } - return nil -} diff --git a/pkg/octavia/pkcs8_aes.go b/pkg/octavia/pkcs8_aes.go new file mode 100644 index 00000000..9bc398e7 --- /dev/null +++ b/pkg/octavia/pkcs8_aes.go @@ -0,0 +1,154 @@ +/* +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 octavia + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/asn1" + "encoding/pem" + "fmt" + + "golang.org/x/crypto/pbkdf2" +) + +var ( + // key derivation functions + oidPKCS5PBKDF2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 12} + oidPBES2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} + oidHMACWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 113549, 2, 9} + + // encryption + oidAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42} +) + +// Encrypted pkcs8 +// Based on https://github.com/youmark/pkcs8 +// MIT license +type prfParam struct { + Algo asn1.ObjectIdentifier + NullParam asn1.RawValue +} + +type pbkdf2Params struct { + Salt []byte + IterationCount int + PrfParam prfParam `asn1:"optional"` +} + +type pbkdf2Algorithms struct { + Algo asn1.ObjectIdentifier + PBKDF2Params pbkdf2Params +} + +type pbkdf2Encs struct { + EncryAlgo asn1.ObjectIdentifier + IV []byte +} + +type pbes2Params struct { + KeyDerivationFunc pbkdf2Algorithms + EncryptionScheme pbkdf2Encs +} + +type encryptedlAlgorithmIdentifier struct { + Algorithm asn1.ObjectIdentifier + Parameters pbes2Params +} + +type encryptedPrivateKeyInfo struct { + Algo encryptedlAlgorithmIdentifier + PrivateKey []byte +} + +// EncryptPrivateKey encrypts given private key data using AES in PKCS#8 format +func EncryptPrivateKey(data, password []byte) (*pem.Block, error) { + pbkdf2Iterations := 600000 + aes256Keysize := 32 + dataLength := len(data) + padSize := aes.BlockSize - dataLength%aes.BlockSize + encryptedSize := dataLength + padSize + + // Generate salt using random data + pbkdf2Salt := make([]byte, 16) + _, err := rand.Read(pbkdf2Salt) + if err != nil { + err = fmt.Errorf("error generating random data for salt for private key encryption: %w", err) + return nil, err + } + + iv := make([]byte, aes.BlockSize) + _, err = rand.Read(iv) + if err != nil { + err = fmt.Errorf("error generating random data for init vector for private key encryption: %w", err) + return nil, err + } + + key := pbkdf2.Key(password, pbkdf2Salt, pbkdf2Iterations, + aes256Keysize, sha256.New) + encryptedBlock, err := aes.NewCipher(key) + if err != nil { + err = fmt.Errorf("error creating new cipher block for private key encryption: %w", err) + return nil, err + } + + cryptSource := make([]byte, encryptedSize) + copy(cryptSource, data) + // Set padding data according to RFC1423, 1.1 + for i := dataLength; i < encryptedSize; i++ { + cryptSource[i] = byte(padSize) + } + encrypted := make([]byte, encryptedSize) + + encrypter := cipher.NewCBCEncrypter(encryptedBlock, iv) + encrypter.CryptBlocks(encrypted, cryptSource) + + // Build encrypted ans1 data + pki := encryptedPrivateKeyInfo{ + Algo: encryptedlAlgorithmIdentifier{ + Algorithm: oidPBES2, + Parameters: pbes2Params{ + KeyDerivationFunc: pbkdf2Algorithms{ + Algo: oidPKCS5PBKDF2, + PBKDF2Params: pbkdf2Params{ + Salt: pbkdf2Salt, + IterationCount: pbkdf2Iterations, + PrfParam: prfParam{ + Algo: oidHMACWithSHA256, + NullParam: asn1.NullRawValue, + }, + }, + }, + EncryptionScheme: pbkdf2Encs{ + EncryAlgo: oidAES256CBC, + IV: iv, + }, + }, + }, + PrivateKey: encrypted, + } + + b, err := asn1.Marshal(pki) + if err != nil { + err = fmt.Errorf("error marshaling encrypted key: %w", err) + return nil, err + } + return &pem.Block{ + Type: "ENCRYPTED PRIVATE KEY", + Bytes: b, + }, nil +}