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/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||||
github.com/stretchr/testify v1.8.2
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
|
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/net v0.0.0-20220418201149-a630d4f3e7a2
|
||||||
golang.org/x/sys v0.6.0
|
golang.org/x/sys v0.6.0
|
||||||
golang.org/x/text v0.8.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/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/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/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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
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=
|
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-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-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-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-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-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
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"
|
||||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
"github.com/qdm12/gluetun/internal/openvpn/pkcs8"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenVPNProviderSettings struct {
|
type OpenVPNProviderSettings struct {
|
||||||
@@ -196,8 +197,20 @@ func OpenVPNConfig(provider OpenVPNProviderSettings,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if *settings.EncryptedKey != "" {
|
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.add("askpass", openvpn.AskPassPath)
|
||||||
lines.addLines(WrapOpenvpnEncryptedKey(*settings.EncryptedKey))
|
lines.addLines(WrapOpenvpnEncryptedKey(encryptedBase64DERKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
if *settings.Cert != "" {
|
if *settings.Cert != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user