diff --git a/go.mod b/go.mod index 9fabd714..16997ea1 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e github.com/stretchr/testify v1.8.2 github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2 golang.org/x/sys v0.6.0 golang.org/x/text v0.8.0 diff --git a/go.sum b/go.sum index df484b7f..4e5011cb 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3C github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg= @@ -136,6 +138,7 @@ golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/internal/openvpn/pkcs8/algorithms.go b/internal/openvpn/pkcs8/algorithms.go new file mode 100644 index 00000000..018467f8 --- /dev/null +++ b/internal/openvpn/pkcs8/algorithms.go @@ -0,0 +1,53 @@ +package pkcs8 + +import ( + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" +) + +var ( + // Algorithm identifiers are listed at + // https://www.ibm.com/docs/en/zos/2.3.0?topic=programming-object-identifiers + oidDESCBC = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 7} //nolint:gochecknoglobals +) + +var ( + ErrEncryptionAlgorithmNotPBES2 = errors.New("encryption algorithm is not PBES2") +) + +type encryptedPrivateKey struct { + EncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedData []byte +} + +type encryptedAlgorithmParams struct { + KeyDerivationFunc pkix.AlgorithmIdentifier + EncryptionScheme pkix.AlgorithmIdentifier +} + +func getEncryptionAlgorithmOid(der []byte) ( + encryptionSchemeAlgorithm asn1.ObjectIdentifier, err error) { + var encryptedPrivateKeyData encryptedPrivateKey + _, err = asn1.Unmarshal(der, &encryptedPrivateKeyData) + if err != nil { + return nil, fmt.Errorf("decoding asn1 encrypted private key data: %w", err) + } + + oidPBES2 := asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} + oidAlgorithm := encryptedPrivateKeyData.EncryptionAlgorithm.Algorithm + if !oidAlgorithm.Equal(oidPBES2) { + return nil, fmt.Errorf("%w: %s instead of PBES2 %s", + ErrEncryptionAlgorithmNotPBES2, oidAlgorithm, oidPBES2) + } + + var encryptionAlgorithmParams encryptedAlgorithmParams + paramBytes := encryptedPrivateKeyData.EncryptionAlgorithm.Parameters.FullBytes + _, err = asn1.Unmarshal(paramBytes, &encryptionAlgorithmParams) + if err != nil { + return nil, fmt.Errorf("decoding asn1 encryption algorithm parameters: %w", err) + } + + return encryptionAlgorithmParams.EncryptionScheme.Algorithm, nil +} diff --git a/internal/openvpn/pkcs8/algorithms_test.go b/internal/openvpn/pkcs8/algorithms_test.go new file mode 100644 index 00000000..66a49f9d --- /dev/null +++ b/internal/openvpn/pkcs8/algorithms_test.go @@ -0,0 +1,106 @@ +package pkcs8 + +import ( + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "errors" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + pkcs8lib "github.com/youmark/pkcs8" +) + +func Test_getEncryptionAlgorithmOid(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + makeDER func() (der []byte, err error) + encryptionSchemeAlgorithm asn1.ObjectIdentifier + errMessage string + }{ + "empty data": { + makeDER: func() (der []byte, err error) { return nil, nil }, + errMessage: "decoding asn1 encrypted private key data: " + + "asn1: syntax error: sequence truncated", + }, + "algorithm not pbes2": { + makeDER: func() (der []byte, err error) { + data := encryptedPrivateKey{ + EncryptionAlgorithm: pkix.AlgorithmIdentifier{ + Algorithm: asn1.ObjectIdentifier{1, 2, 3, 4}, + }, + } + return asn1.Marshal(data) + }, + errMessage: "encryption algorithm is not PBES2: " + + "1.2.3.4 instead of PBES2 1.2.840.113549.1.5.13", + }, + "empty params full bytes": { + makeDER: func() (der []byte, err error) { + data := encryptedPrivateKey{ + EncryptionAlgorithm: pkix.AlgorithmIdentifier{ + Algorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}, + Parameters: asn1.RawValue{ + FullBytes: []byte{}, + }, + }, + } + return asn1.Marshal(data) + }, + errMessage: "decoding asn1 encryption algorithm parameters: " + + "asn1: structure error: tags don't match " + + "(16 vs {class:0 tag:0 length:0 isCompound:false}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} encryptedAlgorithmParams @2", //nolint:lll + }, + "DES-CBC DER": { + makeDER: func() (der []byte, err error) { + DESCBCEncryptedPEM, err := os.ReadFile("testdata/rsa_pkcs8_descbc_encrypted.pem") + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + pemBlock, _ := pem.Decode(DESCBCEncryptedPEM) + if pemBlock == nil { + return nil, errors.New("failed to decode PEM") + } + return pemBlock.Bytes, nil + }, + encryptionSchemeAlgorithm: oidDESCBC, + }, + "AES-128-CBC DER": { + makeDER: func() (der []byte, err error) { + AES128CBCEncryptedPEM, err := os.ReadFile("testdata/rsa_pkcs8_aes128cbc_encrypted.pem") + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + pemBlock, _ := pem.Decode(AES128CBCEncryptedPEM) + if pemBlock == nil { + return nil, errors.New("failed to decode PEM") + } + return pemBlock.Bytes, nil + }, + encryptionSchemeAlgorithm: pkcs8lib.AES128CBC.OID(), + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + der, err := testCase.makeDER() + require.NoError(t, err) + + encryptionSchemeAlgorithm, err := getEncryptionAlgorithmOid(der) + + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + } else { + assert.NoError(t, err) + } + assert.Equal(t, testCase.encryptionSchemeAlgorithm, encryptionSchemeAlgorithm) + }) + } +} diff --git a/internal/openvpn/pkcs8/descbc.go b/internal/openvpn/pkcs8/descbc.go new file mode 100644 index 00000000..e5f91444 --- /dev/null +++ b/internal/openvpn/pkcs8/descbc.go @@ -0,0 +1,59 @@ +package pkcs8 + +import ( + "bytes" + "crypto/cipher" + "crypto/des" //nolint:gosec + "encoding/asn1" + "fmt" + + pkcs8lib "github.com/youmark/pkcs8" +) + +func init() { //nolint:gochecknoinits + pkcs8lib.RegisterCipher(oidDESCBC, newCipherDESCBCBlock) +} + +func newCipherDESCBCBlock() pkcs8lib.Cipher { //nolint:ireturn + return cipherDESCBC{} +} + +type cipherDESCBC struct{} + +func (c cipherDESCBC) IVSize() int { + return des.BlockSize +} + +func (c cipherDESCBC) KeySize() int { + return 8 //nolint:gomnd +} + +func (c cipherDESCBC) OID() asn1.ObjectIdentifier { + return oidDESCBC +} + +func (c cipherDESCBC) Encrypt(key, iv, plaintext []byte) ([]byte, error) { + block, err := des.NewCipher(key) //nolint:gosec + if err != nil { + return nil, fmt.Errorf("creating DES cipher: %w", err) + } + blockEncrypter := cipher.NewCBCEncrypter(block, iv) + paddingLen := block.BlockSize() - (len(plaintext) % block.BlockSize()) + ciphertext := make([]byte, len(plaintext)+paddingLen) + copy(ciphertext, plaintext) + copy(ciphertext[len(plaintext):], + bytes.Repeat([]byte{byte(paddingLen)}, paddingLen)) + blockEncrypter.CryptBlocks(ciphertext, ciphertext) + return ciphertext, nil +} + +func (c cipherDESCBC) Decrypt(key, iv, ciphertext []byte) ([]byte, error) { + block, err := des.NewCipher(key) //nolint:gosec + if err != nil { + return nil, fmt.Errorf("creating DES cipher: %w", err) + } + blockDecrypter := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(ciphertext)) + blockDecrypter.CryptBlocks(plaintext, ciphertext) + return plaintext, nil +} diff --git a/internal/openvpn/pkcs8/testdata/readme.txt b/internal/openvpn/pkcs8/testdata/readme.txt new file mode 100644 index 00000000..9eca7c08 --- /dev/null +++ b/internal/openvpn/pkcs8/testdata/readme.txt @@ -0,0 +1,12 @@ +The key files in this directory are generated using OpenSSL. +Re-generating them is fine and should work with existing tests. + +For DES encrypted RSA key files, openssl version 1.x.x is required, and the following commands in order generate the files: + +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:512 -des -pass pass:password -out rsa_pkcs8_aes128cbc_encrypted.pem +openssl pkcs8 -topk8 -in rsa_pkcs8_aes128cbc_encrypted.pem -passin pass:password -nocrypt -out rsa_pkcs8_aes128cbc_decrypted.pem + +For AES encrypted RSA key files, the following commands in order generate the files: + +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:512 -aes-128-cbc -pass pass:password -out rsa_pkcs8_descbc_encrypted.pem +openssl pkcs8 -topk8 -in rsa_pkcs8_descbc_encrypted.pem -passin pass:password -nocrypt -out rsa_pkcs8_descbc_decrypted.pem diff --git a/internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_decrypted.pem b/internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_decrypted.pem new file mode 100644 index 00000000..cb5de920 --- /dev/null +++ b/internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_decrypted.pem @@ -0,0 +1,10 @@ +-----BEGIN PRIVATE KEY----- +MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEAsont6TMS9RVqjXoi +wF/oKZCwbWM4HmCJvp5Z2dOfKabt+7FOTJiD7APLJKva6791HDTuyBu7+HFQCzW3 +ghLuiwIDAQABAkB09FuwHq/1cmEJao+nO2xHBiw8i/lwFMdG4k5znegujL4g16i7 ++afWrMd54jYNPGiKuSNObB2BZR1j8tz/jvbxAiEA3d7bVwtWdaZVIV5t9uqrq5fG +j3eXfNemTu1HQDmVqNMCIQDOALECY98KURR4NJueTKNuvawkuWFhizfKKTfS5B6Q +aQIhANsF/RFYp+lMYg2m4nc2AnJKSkGmlW0wlYSkyAmmzw7xAiEAqSz+MSVNnU5a +ziD+D/GGYkKYJYysgYvwZDCXbLT0uMkCIQDZghteTq2MMwIWWUJti3nc6nCICaJu +d5O9Sm7BcOSuoA== +-----END PRIVATE KEY----- diff --git a/internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_encrypted.pem b/internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_encrypted.pem new file mode 100644 index 00000000..9524bbc6 --- /dev/null +++ b/internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_encrypted.pem @@ -0,0 +1,12 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIX7rAZ9pfZ4ACAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBBVQ5G606jCKrKADBAiKwcPBIIB +YBbudvVfqdLKm9LBFOAcUQk+sdFrq6e2r/xnuqM7VY6Ru4pMOmVMhHMMCFkqHLjx +f7hN+xjk3XpYyoptnozPBOhypZrjd6IeEJSkBtU5BZR8fP0Bhny5NYHGcyPR6MZA +5iX/0fnyMlrncG67UNwoZQjfg7jEO3mAjuCW/F74xtPQ90ZHtw8mYC26fa09uQR4 +ptL9XqZuw4+U//3CuOheKqI17wulKAb4NwJckYbKyOik+J4yAi0ScgO73pD1FFvl +qBxcpyvEqFQqkOlcbR9YwVBAXeW8cbpZJd+MilSs7Ru/phHrP9wz5chYDrocbeG/ +H8FhCCvZnJ3zC3P3FPRNPtoaduJ0MbYpaMv4hyP3tEbzbslPA1v14ES3U+w0gmdD +zpsy0oplQK9d9wL2TKBwyALcUx5BhtcqKsUXwBOWXMToc4lIXUVl0UVYwULibmEd +yK6ajugNxG95X+BJjGvWu/U= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_decrypted.pem b/internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_decrypted.pem new file mode 100644 index 00000000..7d50af87 --- /dev/null +++ b/internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_decrypted.pem @@ -0,0 +1,10 @@ +-----BEGIN PRIVATE KEY----- +MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEAuU3FTtbPm8OjZ/d8 +vVd+seQcrCGgwxigKpOszFfOOXKxfy2CgpjE1Ga2h0UneJ6pq0KZyY+ggYAX8PaS +U6R3HwIDAQABAkEAibQPkjzz3u8Nua8i1Zn1nsDDxe7fhtv/+mPvn5MIv4sFRS71 +0o9+SPNIQn7aJcGIqyBzHYdQg3/wGla+LA+msQIhAOt+hy1dnaWTSXIrIuPt+sSP +Fjk80ijfxntXHNU6qExjAiEAyXBurrTdQs6D61ZzdlOFzgUs/FHa4dmWmxXuFsdv +8RUCIQCIZQJaLiyOp94UOBO/PCjQC6ftguKeNe25plzWy2CKzQIgXBpBMTZXGG2u +WZMcldSYkFtDd1bB2pQPTXeYdefYYgUCIQDVH3ysySFXIlHJulgcxvriXTfY4goY +TQ0PL0Ow7sIz6A== +-----END PRIVATE KEY----- diff --git a/internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_encrypted.pem b/internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_encrypted.pem new file mode 100644 index 00000000..37d8b916 --- /dev/null +++ b/internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_encrypted.pem @@ -0,0 +1,12 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIBsTBLBgkqhkiG9w0BBQ0wPjApBgkqhkiG9w0BBQwwHAQIZK8yPqcvVqoCAggA +MAwGCCqGSIb3DQIJBQAwEQYFKw4DAgcECI7C8b+gk6UJBIIBYGQQ4UcglyUqSFC7 +JiA+Gh01K1odfdLJKLh30+iescrFenII4Vv4rX5609URhn2iHCXhlnZ0+9geRR9k +dQSKXaDVVGQw3bQUKgS+lZDAeLV4PS7c+KW0xLpXWJxBPs6NXQMxoJZ23UA391EH +p8gKzZqUKk/rEOP68wr3IpHqaD3xggzN+4eA4ZKj4OktmWfUjgC7RQIZSaMxfq+D +q+4D5onp+B4C2WRfjnN/N2g7UhzKWGvhjKyogvl82PuY9Vp1qPwQGdg5wdJ/2UVX +QNvbkT21Wrv1ffFuIDS1/lCPnd8RAl2Q7chfLyut4BjP0tlmYNxRwQU2mT3KZOrB +wwhWgXZtBwj4LjyasVkKe4hyVfRXN5NgONvqxof3VdZUHzOegOapNbEmfhNwVogj +1gwRWL7etAbYKjiMPFzZJAiU97+UkqveguldeoHmvWRDTLqxgZw5M4wkPPldb+u8 +d1vCDDQ= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/internal/openvpn/pkcs8/upgrade.go b/internal/openvpn/pkcs8/upgrade.go new file mode 100644 index 00000000..54693f2a --- /dev/null +++ b/internal/openvpn/pkcs8/upgrade.go @@ -0,0 +1,52 @@ +package pkcs8 + +import ( + "encoding/base64" + "errors" + "fmt" + + pkcs8lib "github.com/youmark/pkcs8" +) + +var ( + ErrUnsupportedKeyType = errors.New("unsupported key type") +) + +// UpgradeEncryptedKey eventually upgrades an encrypted key to a newer encryption +// if its encryption is too weak for Openvpn/Openssl. +// If the key is encrypted using DES-CBC, it is decrypted and re-encrypted using AES-256-CBC. +// Otherwise, the key is returned unmodified. +// Note this function only supports: +// - PKCS8 encrypted keys +// - RSA and ECDSA keys +// - DES-CBC, 3DES, AES-128-CBC, AES-192-CBC, AES-256-CBC, AES-128-GCM, AES-192-GCM +// and AES-256-GCM encryption algorithms. +func UpgradeEncryptedKey(encryptedPKCS8DERKey, passphrase string) (securelyEncryptedPKCS8DERKey string, err error) { + der, err := base64.StdEncoding.DecodeString(encryptedPKCS8DERKey) + if err != nil { + return "", fmt.Errorf("decoding base64 encoded DER: %w", err) + } + + oidEncryptionAlgorithm, err := getEncryptionAlgorithmOid(der) + if err != nil { + return "", fmt.Errorf("finding encryption algorithm oid: %w", err) + } + + if !oidEncryptionAlgorithm.Equal(oidDESCBC) { + return encryptedPKCS8DERKey, nil + } + + // Convert DES-CBC encrypted key to an AES256CBC encrypted key + privateKey, err := pkcs8lib.ParsePKCS8PrivateKey(der, []byte(passphrase)) + if err != nil { + return "", fmt.Errorf("parsing pkcs8 encrypted private key: %w", err) + } + + der, err = pkcs8lib.MarshalPrivateKey(privateKey, []byte(passphrase), pkcs8lib.DefaultOpts) + if err != nil { + return "", fmt.Errorf("encrypting and encoding private key: %w", err) + } + + securelyEncryptedPKCS8DERKey = base64.StdEncoding.EncodeToString(der) + return securelyEncryptedPKCS8DERKey, nil +} diff --git a/internal/openvpn/pkcs8/upgrade_test.go b/internal/openvpn/pkcs8/upgrade_test.go new file mode 100644 index 00000000..8fc03614 --- /dev/null +++ b/internal/openvpn/pkcs8/upgrade_test.go @@ -0,0 +1,75 @@ +package pkcs8 + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/youmark/pkcs8" +) + +func parsePEMFile(t *testing.T, pemFilepath string) (base64DER string) { + t.Helper() + + bytes, err := os.ReadFile(pemFilepath) + require.NoError(t, err) + + pemBlock, _ := pem.Decode(bytes) + require.NotNil(t, pemBlock) + + derBytes := pemBlock.Bytes + base64DER = base64.StdEncoding.EncodeToString(derBytes) + return base64DER +} + +func Test_UpgradeEncryptedKey(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + encryptedPKCS8base64DERKey string + passphrase string + decryptedPKCS8Base64DERKey string + errMessage string + }{ + "AES-128-CBC key": { + encryptedPKCS8base64DERKey: parsePEMFile(t, "testdata/rsa_pkcs8_aes128cbc_encrypted.pem"), + passphrase: "password", + decryptedPKCS8Base64DERKey: parsePEMFile(t, "testdata/rsa_pkcs8_aes128cbc_decrypted.pem"), + }, + "DES-CBC key": { + encryptedPKCS8base64DERKey: parsePEMFile(t, "testdata/rsa_pkcs8_descbc_encrypted.pem"), + passphrase: "password", + decryptedPKCS8Base64DERKey: parsePEMFile(t, "testdata/rsa_pkcs8_descbc_decrypted.pem"), + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + securelyEncryptedPKCS8DERKey, err := UpgradeEncryptedKey(testCase.encryptedPKCS8base64DERKey, testCase.passphrase) + + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + return + } + assert.NoError(t, err) + + // Decrypt possible re-encrypted key to verify it matches the expected + // corresponding decrypted key. + der, err := base64.StdEncoding.DecodeString(securelyEncryptedPKCS8DERKey) + require.NoError(t, err) + privateKey, err := pkcs8.ParsePKCS8PrivateKey(der, []byte(testCase.passphrase)) + require.NoError(t, err) + der, err = x509.MarshalPKCS8PrivateKey(privateKey) + require.NoError(t, err) + base64DER := base64.StdEncoding.EncodeToString(der) + assert.Equal(t, testCase.decryptedPKCS8Base64DERKey, base64DER) + }) + } +} diff --git a/internal/provider/utils/openvpn.go b/internal/provider/utils/openvpn.go index 8f6e5655..6bf012a1 100644 --- a/internal/provider/utils/openvpn.go +++ b/internal/provider/utils/openvpn.go @@ -8,6 +8,7 @@ import ( "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants/openvpn" "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/openvpn/pkcs8" ) type OpenVPNProviderSettings struct { @@ -196,8 +197,20 @@ func OpenVPNConfig(provider OpenVPNProviderSettings, } if *settings.EncryptedKey != "" { + encryptedBase64DERKey := *settings.EncryptedKey + if settings.Version != openvpn.Openvpn24 { + // OpenVPN above 2.4 does not support old encryption schemes such as + // DES-CBC, so decrypt and reencrypt the key. + // This is a workaround for VPN secure. + var err error + encryptedBase64DERKey, err = pkcs8.UpgradeEncryptedKey(encryptedBase64DERKey, *settings.KeyPassphrase) + if err != nil { + // TODO return an error instead. + panic(fmt.Sprintf("upgrading encrypted key: %s", err)) + } + } lines.add("askpass", openvpn.AskPassPath) - lines.addLines(WrapOpenvpnEncryptedKey(*settings.EncryptedKey)) + lines.addLines(WrapOpenvpnEncryptedKey(encryptedBase64DERKey)) } if *settings.Cert != "" {