fix(vpnsecure): upgrade Openvpn key encryption if needed (#1471)
This commit is contained in:
1
go.mod
1
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
|
||||
|
||||
3
go.sum
3
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=
|
||||
|
||||
53
internal/openvpn/pkcs8/algorithms.go
Normal file
53
internal/openvpn/pkcs8/algorithms.go
Normal 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
|
||||
}
|
||||
106
internal/openvpn/pkcs8/algorithms_test.go
Normal file
106
internal/openvpn/pkcs8/algorithms_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
59
internal/openvpn/pkcs8/descbc.go
Normal file
59
internal/openvpn/pkcs8/descbc.go
Normal 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
|
||||
}
|
||||
12
internal/openvpn/pkcs8/testdata/readme.txt
vendored
Normal file
12
internal/openvpn/pkcs8/testdata/readme.txt
vendored
Normal 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
|
||||
10
internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_decrypted.pem
vendored
Normal file
10
internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_decrypted.pem
vendored
Normal 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-----
|
||||
12
internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_encrypted.pem
vendored
Normal file
12
internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_encrypted.pem
vendored
Normal 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-----
|
||||
10
internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_decrypted.pem
vendored
Normal file
10
internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_decrypted.pem
vendored
Normal 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-----
|
||||
12
internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_encrypted.pem
vendored
Normal file
12
internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_encrypted.pem
vendored
Normal 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-----
|
||||
52
internal/openvpn/pkcs8/upgrade.go
Normal file
52
internal/openvpn/pkcs8/upgrade.go
Normal 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
|
||||
}
|
||||
75
internal/openvpn/pkcs8/upgrade_test.go
Normal file
75
internal/openvpn/pkcs8/upgrade_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 != "" {
|
||||
|
||||
Reference in New Issue
Block a user