Skip to content

Commit d5e74a1

Browse files
committed
security: replace weak 3DES with AES-256-CBC encryption in PKCS12
1 parent e2f2c07 commit d5e74a1

6 files changed

Lines changed: 251 additions & 13 deletions

File tree

builder/azure/arm/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ func (c *Config) formatCertificateForKeyVault(privateKey *rsa.PrivateKey) (strin
817817
return "", err
818818
}
819819

820-
pfxBytes, err := pkcs12.Encode(derBytes, privateKey, c.tmpCertificatePassword)
820+
pfxBytes, err := pkcs12.EncodeModern(derBytes, privateKey, c.tmpCertificatePassword)
821821
if err != nil {
822822
err = fmt.Errorf("Failed to encode certificate as PFX: %s", err)
823823
return "", err

builder/azure/dtl/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ func (c *Config) createCertificate() (string, string, error) {
377377
return "", "", err
378378
}
379379

380-
pfxBytes, err := pkcs12.Encode(derBytes, privateKey, c.tmpCertificatePassword)
380+
pfxBytes, err := pkcs12.EncodeModern(derBytes, privateKey, c.tmpCertificatePassword)
381381
if err != nil {
382382
err = fmt.Errorf("Failed to encode certificate as PFX: %s", err)
383383
return "", "", err

builder/azure/pkcs12/crypto.go

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@ package pkcs12
66

77
import (
88
"bytes"
9+
"crypto/aes"
910
"crypto/cipher"
1011
"crypto/des"
1112
"crypto/rand"
13+
"crypto/sha256"
1214
"crypto/x509/pkix"
1315
"encoding/asn1"
1416
"errors"
1517
"io"
1618

1719
"github.com/hashicorp/packer-plugin-azure/builder/azure/pkcs12/rc2"
20+
"golang.org/x/crypto/pbkdf2"
1821
)
1922

2023
const (
21-
pbeIterationCount = 2048
22-
pbeSaltSizeBytes = 8
24+
pbeIterationCount = 2048
25+
pbeIterationCountModern = 100000 // OWASP 2021 baseline for PBKDF2, balances security and performance
26+
pbeSaltSizeBytes = 8
2327
)
2428

2529
var (
@@ -65,6 +69,21 @@ func (shaWith40BitRC2CBC) deriveIV(salt, password []byte, iterations int) []byte
6569
return pbkdf(sha1Sum, 20, 64, salt, password, iterations, 2, 8)
6670
}
6771

72+
// Modern AES-256-CBC cipher using PBKDF2
73+
type aes256CBC struct{}
74+
75+
func (aes256CBC) create(key []byte) (cipher.Block, error) {
76+
return aes.NewCipher(key)
77+
}
78+
79+
func (aes256CBC) deriveKey(salt, password []byte, iterations int) []byte {
80+
return pbkdf2.Key(password, salt, iterations, 32, sha256.New)
81+
}
82+
83+
func (aes256CBC) deriveIV(salt, password []byte, iterations int) []byte {
84+
return pbkdf2.Key(password, salt, iterations, 16, sha256.New)
85+
}
86+
6887
type pbeParams struct {
6988
Salt []byte
7089
Iterations int
@@ -73,20 +92,25 @@ type pbeParams struct {
7392
func pbDecrypterFor(algorithm pkix.AlgorithmIdentifier, password []byte) (cipher.BlockMode, int, error) {
7493
var cipherType pbeCipher
7594

76-
switch {
77-
case algorithm.Algorithm.Equal(oidPBEWithSHAAnd3KeyTripleDESCBC):
78-
cipherType = shaWithTripleDESCBC{}
79-
case algorithm.Algorithm.Equal(oidPBEWithSHAAnd40BitRC2CBC):
80-
cipherType = shaWith40BitRC2CBC{}
81-
default:
82-
return nil, 0, NotImplementedError("algorithm " + algorithm.Algorithm.String() + " is not supported")
83-
}
84-
8595
var params pbeParams
8696
if err := unmarshal(algorithm.Parameters.FullBytes, &params); err != nil {
8797
return nil, 0, err
8898
}
8999

100+
// Detect modern AES-256-CBC encryption by high iteration count
101+
if params.Iterations >= pbeIterationCountModern {
102+
cipherType = aes256CBC{}
103+
} else {
104+
switch {
105+
case algorithm.Algorithm.Equal(oidPBEWithSHAAnd3KeyTripleDESCBC):
106+
cipherType = shaWithTripleDESCBC{}
107+
case algorithm.Algorithm.Equal(oidPBEWithSHAAnd40BitRC2CBC):
108+
cipherType = shaWith40BitRC2CBC{}
109+
default:
110+
return nil, 0, NotImplementedError("algorithm " + algorithm.Algorithm.String() + " is not supported")
111+
}
112+
}
113+
90114
key := cipherType.deriveKey(params.Salt, password, params.Iterations)
91115
iv := cipherType.deriveIV(params.Salt, password, params.Iterations)
92116

@@ -137,6 +161,8 @@ func pad(src []byte, blockSize int) []byte {
137161
return append(src, paddingText...)
138162
}
139163

164+
// pbEncrypt encrypts plainText using legacy Triple DES algorithm.
165+
// Deprecated: Use pbEncryptModern for AES-256-CBC encryption instead.
140166
func pbEncrypt(plainText, salt, password []byte, iterations int) (cipherText []byte, err error) {
141167
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
142168
return nil, errors.New("pkcs12: failed to create a random salt value: " + err.Error())
@@ -160,6 +186,33 @@ func pbEncrypt(plainText, salt, password []byte, iterations int) (cipherText []b
160186
return cipherText, nil
161187
}
162188

189+
// pbEncryptModern encrypts plainText using modern AES-256-CBC algorithm with PBKDF2.
190+
// This provides much stronger security than the legacy Triple DES implementation.
191+
// Uses 100,000 iterations (OWASP 2021 baseline), providing strong security while
192+
// maintaining reasonable performance for ephemeral certificates.
193+
func pbEncryptModern(plainText, salt, password []byte, iterations int) (cipherText []byte, err error) {
194+
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
195+
return nil, errors.New("pkcs12: failed to create a random salt value: " + err.Error())
196+
}
197+
198+
cipherType := aes256CBC{}
199+
key := cipherType.deriveKey(salt, password, iterations)
200+
iv := cipherType.deriveIV(salt, password, iterations)
201+
202+
block, err := cipherType.create(key)
203+
if err != nil {
204+
return nil, errors.New("pkcs12: failed to create a block cipher: " + err.Error())
205+
}
206+
207+
paddedPlainText := pad(plainText, block.BlockSize())
208+
209+
encrypter := cipher.NewCBCEncrypter(block, iv)
210+
cipherText = make([]byte, len(paddedPlainText))
211+
encrypter.CryptBlocks(cipherText, paddedPlainText)
212+
213+
return cipherText, nil
214+
}
215+
163216
// decryptable abstracts a object that contains ciphertext.
164217
type decryptable interface {
165218
Algorithm() pkix.AlgorithmIdentifier

builder/azure/pkcs12/pkcs12.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,40 @@ func makeContentInfos(derBytes []byte, privateKey interface{}, password []byte)
381381
return contentInfos, nil
382382
}
383383

384+
// makeShroudedKeyBagContentInfoModern creates content info using modern AES-256-CBC encryption.
385+
func makeShroudedKeyBagContentInfoModern(privateKey interface{}, password []byte) (*contentInfo, error) {
386+
shroudedKeyBagBytes, err := encodePkcs8ShroudedKeyBagModern(privateKey, password)
387+
if err != nil {
388+
return nil, EncodeError("encode PKCS#8 shrouded key bag: " + err.Error())
389+
}
390+
391+
safeBags, err := makeSafeBags(oidPKCS8ShroudedKeyBag, shroudedKeyBagBytes)
392+
if err != nil {
393+
return nil, EncodeError("safe bags: " + err.Error())
394+
}
395+
396+
return makeContentInfo(safeBags)
397+
}
398+
399+
// makeContentInfosModern creates content infos using modern AES-256-CBC encryption.
400+
func makeContentInfosModern(derBytes []byte, privateKey interface{}, password []byte) ([]contentInfo, error) {
401+
shroudedKeyContentInfo, err := makeShroudedKeyBagContentInfoModern(privateKey, password)
402+
if err != nil {
403+
return nil, EncodeError("shrouded key content info: " + err.Error())
404+
}
405+
406+
certBagContentInfo, err := makeCertBagContentInfo(derBytes)
407+
if err != nil {
408+
return nil, EncodeError("cert bag content info: " + err.Error())
409+
}
410+
411+
contentInfos := make([]contentInfo, 2)
412+
contentInfos[0] = *shroudedKeyContentInfo
413+
contentInfos[1] = *certBagContentInfo
414+
415+
return contentInfos, nil
416+
}
417+
384418
func makeSalt(saltByteCount int) ([]byte, error) {
385419
salt := make([]byte, saltByteCount)
386420
_, err := io.ReadFull(rand.Reader, salt)
@@ -453,6 +487,75 @@ func Encode(derBytes []byte, privateKey interface{}, password string) (pfxBytes
453487
return bytes, err
454488
}
455489

490+
// EncodeModern converts a certificate and a private key to the PKCS#12 byte stream format
491+
// using modern AES-256-CBC encryption instead of the legacy Triple DES algorithm.
492+
// This provides much stronger security for the encoded certificate.
493+
//
494+
// derBytes is a DER encoded certificate.
495+
// privateKey is an RSA or ECDSA private key.
496+
// password is the password to encrypt the private key.
497+
func EncodeModern(derBytes []byte, privateKey interface{}, password string) (pfxBytes []byte, err error) {
498+
secret, err := bmpString(password)
499+
if err != nil {
500+
return nil, ErrIncorrectPassword
501+
}
502+
503+
contentInfos, err := makeContentInfosModern(derBytes, privateKey, secret)
504+
if err != nil {
505+
return nil, err
506+
}
507+
508+
// Marshal []contentInfo so we can re-constitute the byte stream that will
509+
// be suitable for computing the MAC
510+
bytes, err := asn1.Marshal(contentInfos)
511+
if err != nil {
512+
return nil, err
513+
}
514+
515+
// Unmarshal as an asn1.RawValue so, we can compute the MAC against the .Bytes
516+
var contentInfosRaw asn1.RawValue
517+
err = unmarshal(bytes, &contentInfosRaw)
518+
if err != nil {
519+
return nil, err
520+
}
521+
522+
authSafeContentInfo, err := makeContentInfo(contentInfosRaw)
523+
if err != nil {
524+
return nil, EncodeError("authSafe content info: " + err.Error())
525+
}
526+
527+
salt, err := makeSalt(pbeSaltSizeBytes)
528+
if err != nil {
529+
return nil, EncodeError("salt value: " + err.Error())
530+
}
531+
532+
// Compute the MAC for marshaled bytes of contentInfos, which includes the
533+
// cert bag, and the shrouded key bag.
534+
digest := computeMac(contentInfosRaw.FullBytes, pbeIterationCount, salt, secret)
535+
536+
pfx := pfxPdu{
537+
Version: 3,
538+
AuthSafe: *authSafeContentInfo,
539+
MacData: macData{
540+
Iterations: pbeIterationCount,
541+
MacSalt: salt,
542+
Mac: digestInfo{
543+
Algorithm: pkix.AlgorithmIdentifier{
544+
Algorithm: oidSHA1,
545+
},
546+
Digest: digest,
547+
},
548+
},
549+
}
550+
551+
bytes, err = asn1.Marshal(pfx)
552+
if err != nil {
553+
return nil, EncodeError("marshal PFX PDU: " + err.Error())
554+
}
555+
556+
return bytes, err
557+
}
558+
456559
func getSafeContents(p12Data, password []byte) (bags []safeBag, updatedPassword []byte, err error) {
457560
pfx := new(pfxPdu)
458561

builder/azure/pkcs12/pkcs12_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,46 @@ func testPfxRoundTrip(t *testing.T, privateKey interface{}) interface{} {
9494
return key
9595
}
9696

97+
// TestEncodeModernRsa tests encoding with the modern AES-256-CBC encryption
98+
func TestEncodeModernRsa(t *testing.T) {
99+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
100+
if err != nil {
101+
t.Fatal(err.Error())
102+
}
103+
104+
certificateBytes, err := newCertificate("test.example.com", privateKey)
105+
if err != nil {
106+
t.Fatal(err.Error())
107+
}
108+
109+
// Test with EncodeModern (AES-256-CBC)
110+
pfxBytes, err := EncodeModern(certificateBytes, privateKey, "testpassword")
111+
if err != nil {
112+
t.Fatalf("EncodeModern failed: %v", err)
113+
}
114+
115+
// Verify we can decode it back
116+
decodedKey, cert, err := Decode(pfxBytes, "testpassword")
117+
if err != nil {
118+
t.Fatalf("Failed to decode modern encoded PFX: %v", err)
119+
}
120+
121+
// Verify the private key matches
122+
actualPrivateKey, ok := decodedKey.(*rsa.PrivateKey)
123+
if !ok {
124+
t.Fatalf("failed to decode private key")
125+
}
126+
127+
if privateKey.D.Cmp(actualPrivateKey.D) != 0 {
128+
t.Errorf("Private key D component doesn't match")
129+
}
130+
131+
// Verify the certificate matches
132+
if cert.Subject.CommonName != "test.example.com" {
133+
t.Errorf("expected common name to be %q, but found %q", "test.example.com", cert.Subject.CommonName)
134+
}
135+
}
136+
97137
func TestPEM(t *testing.T) {
98138
for commonName, base64P12 := range testdata {
99139
p12, _ := base64.StdEncoding.DecodeString(base64P12)

builder/azure/pkcs12/safebags.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,48 @@ func encodePkcs8ShroudedKeyBag(privateKey interface{}, password []byte) (bytes [
7070
return bytes, err
7171
}
7272

73+
// encodePkcs8ShroudedKeyBagModern encodes a private key using modern AES-256-CBC encryption.
74+
// This provides much stronger security than the legacy Triple DES implementation.
75+
func encodePkcs8ShroudedKeyBagModern(privateKey interface{}, password []byte) (bytes []byte, err error) {
76+
privateKeyBytes, err := marshalPKCS8PrivateKey(privateKey)
77+
78+
if err != nil {
79+
return nil, errors.New("pkcs12: error encoding PKCS#8 private key: " + err.Error())
80+
}
81+
82+
salt, err := makeSalt(pbeSaltSizeBytes)
83+
if err != nil {
84+
return nil, errors.New("pkcs12: error creating PKCS#8 salt: " + err.Error())
85+
}
86+
87+
pkData, err := pbEncryptModern(privateKeyBytes, salt, password, pbeIterationCountModern)
88+
if err != nil {
89+
return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key bag when encrypting cert bag: " + err.Error())
90+
}
91+
92+
// Use high iteration count in params so decoder can detect modern encryption
93+
params, err := getAlgorithmParams(salt, pbeIterationCountModern)
94+
if err != nil {
95+
return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key bag algorithm's parameters: " + err.Error())
96+
}
97+
98+
pkinfo := encryptedPrivateKeyInfo{
99+
AlgorithmIdentifier: pkix.AlgorithmIdentifier{
100+
// Keep same OID for compatibility, but use high iteration count to signal AES-256
101+
Algorithm: oidPBEWithSHAAnd3KeyTripleDESCBC,
102+
Parameters: params,
103+
},
104+
EncryptedData: pkData,
105+
}
106+
107+
bytes, err = asn1.Marshal(pkinfo)
108+
if err != nil {
109+
return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key bag: " + err.Error())
110+
}
111+
112+
return bytes, err
113+
}
114+
73115
func decodePkcs8ShroudedKeyBag(asn1Data, password []byte) (privateKey interface{}, err error) {
74116
pkinfo := new(encryptedPrivateKeyInfo)
75117
if err = unmarshal(asn1Data, pkinfo); err != nil {

0 commit comments

Comments
 (0)