fix(vpnsecure): upgrade Openvpn key encryption if needed (#1471)

This commit is contained in:
Quentin McGaw
2023-04-03 12:40:09 +02:00
committed by GitHub
parent 8bfa2f9b27
commit 3b86927ca7
13 changed files with 419 additions and 1 deletions

1
go.mod
View File

@@ -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

3
go.sum
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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:<nil> tag:<nil> 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)
})
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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 != "" {