From e7828c4ddf54e5a0df421f1d8915e4b04cd67c4d Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Wed, 2 Oct 2024 23:42:47 +0300 Subject: [PATCH] OpenVPN matcher: initial commit --- README.md | 1 + imports.go | 1 + .../gd_matcher_openvpn.caddytest | 169 ++++ modules/l4openvpn/crypto.go | 489 +++++++++ modules/l4openvpn/matcher.go | 633 ++++++++++++ modules/l4openvpn/matcher_test.go | 551 ++++++++++ modules/l4openvpn/messages.go | 948 ++++++++++++++++++ 7 files changed, 2792 insertions(+) create mode 100644 integration/caddyfile_adapt/gd_matcher_openvpn.caddytest create mode 100644 modules/l4openvpn/crypto.go create mode 100644 modules/l4openvpn/matcher.go create mode 100644 modules/l4openvpn/matcher_test.go create mode 100644 modules/l4openvpn/messages.go diff --git a/README.md b/README.md index a7600ae..d7f6a56 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Current matchers: - **layer4.matchers.http** - matches connections that start with HTTP requests. In addition, any [`http.matchers` modules](https://caddyserver.com/docs/modules/) can be used for matching on HTTP-specific properties of requests, such as header or path. Note that only the first request of each connection can be used for matching. - **layer4.matchers.local_ip** - matches connections based on local IP (or CIDR range). - **layer4.matchers.not** - matches connections that aren't matched by inner matcher sets. +- **layer4.matchers.openvpn** - matches connections that look like [OpenVPN](https://openvpn.net/community-resources/openvpn-protocol/) connections. - **layer4.matchers.postgres** - matches connections that look like Postgres connections. - **layer4.matchers.proxy_protocol** - matches connections that start with [HAPROXY proxy protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). - **layer4.matchers.rdp** - matches connections that look like [RDP](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPBCGR/%5BMS-RDPBCGR%5D.pdf). diff --git a/imports.go b/imports.go index 83274d4..6b4c2f8 100644 --- a/imports.go +++ b/imports.go @@ -21,6 +21,7 @@ import ( _ "github.com/mholt/caddy-l4/modules/l4dns" _ "github.com/mholt/caddy-l4/modules/l4echo" _ "github.com/mholt/caddy-l4/modules/l4http" + _ "github.com/mholt/caddy-l4/modules/l4openvpn" _ "github.com/mholt/caddy-l4/modules/l4postgres" _ "github.com/mholt/caddy-l4/modules/l4proxy" _ "github.com/mholt/caddy-l4/modules/l4proxyprotocol" diff --git a/integration/caddyfile_adapt/gd_matcher_openvpn.caddytest b/integration/caddyfile_adapt/gd_matcher_openvpn.caddytest new file mode 100644 index 0000000..8cfe4db --- /dev/null +++ b/integration/caddyfile_adapt/gd_matcher_openvpn.caddytest @@ -0,0 +1,169 @@ +{ + layer4 { + :8843 { + @plain openvpn { + modes plain + } + route @plain { + proxy localhost:1194 + } + @auth openvpn { + modes auth + auth_digest sha256 + group_key_direction normal + group_key_file /etc/openvpn/ta.key + } + route @auth { + proxy localhost:1195 + } + @crypt openvpn { + modes crypt + group_key 21d94830510107f8753d3b6f3145e01ded37075115afcb0538ecdd8503ee96637218c9ed38d908d594231d7d143c73da5055310f89d336da99c8b3dcb18909c79dd44f540670ebc0f120beb7211e96839cb542572c48bfa7ffaa9a22cb8304b7869b92f4442918e598745bb78ac8877f02b00a7cdef3f2446c130d39a7c451269ef399fd6029cdfc80a7c604041312ab0a969bc906bdee6e6d707afdcbe8c7fb97beb66049c3d328340775025433ceba1e38008a826cf92443d903106199373bdadd9c2c735cf481e580db4e81b99f12e3f46b6159c687cd1b9e689f7712573c0f02735a45573dfb5cd55cf4649423892c7e91f439bdd7337a8ceebd302cfbfa + } + route @crypt { + proxy localhost:1196 + } + @crypt2 openvpn { + modes crypt2 + server_key_file /etc/openvpn/v2-server.key + } + route @crypt2 { + proxy localhost:1197 + } + route { + tls + proxy localhost:8080 + } + } + } +} +---------- +{ + "apps": { + "layer4": { + "servers": { + "srv0": { + "listen": [ + ":8843" + ], + "routes": [ + { + "match": [ + { + "openvpn": { + "modes": [ + "plain" + ] + } + } + ], + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "localhost:1194" + ] + } + ] + } + ] + }, + { + "match": [ + { + "openvpn": { + "modes": [ + "auth" + ], + "group_key_file": "/etc/openvpn/ta.key", + "auth_digest": "sha256", + "group_key_direction": "normal" + } + } + ], + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "localhost:1195" + ] + } + ] + } + ] + }, + { + "match": [ + { + "openvpn": { + "modes": [ + "crypt" + ], + "group_key": "21d94830510107f8753d3b6f3145e01ded37075115afcb0538ecdd8503ee96637218c9ed38d908d594231d7d143c73da5055310f89d336da99c8b3dcb18909c79dd44f540670ebc0f120beb7211e96839cb542572c48bfa7ffaa9a22cb8304b7869b92f4442918e598745bb78ac8877f02b00a7cdef3f2446c130d39a7c451269ef399fd6029cdfc80a7c604041312ab0a969bc906bdee6e6d707afdcbe8c7fb97beb66049c3d328340775025433ceba1e38008a826cf92443d903106199373bdadd9c2c735cf481e580db4e81b99f12e3f46b6159c687cd1b9e689f7712573c0f02735a45573dfb5cd55cf4649423892c7e91f439bdd7337a8ceebd302cfbfa" + } + } + ], + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "localhost:1196" + ] + } + ] + } + ] + }, + { + "match": [ + { + "openvpn": { + "modes": [ + "crypt2" + ], + "server_key_file": "/etc/openvpn/v2-server.key" + } + } + ], + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "localhost:1197" + ] + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "tls" + }, + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "localhost:8080" + ] + } + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/modules/l4openvpn/crypto.go b/modules/l4openvpn/crypto.go new file mode 100644 index 0000000..03a6274 --- /dev/null +++ b/modules/l4openvpn/crypto.go @@ -0,0 +1,489 @@ +// Copyright 2024 VNXME +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package l4openvpn + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "errors" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/blake2s" + "golang.org/x/crypto/ripemd160" + "golang.org/x/crypto/sha3" + "hash" + "io" + "os" + "regexp" + "slices" + "strings" +) + +// AuthDigest represents a digest used for computing HMACs of control messages. +type AuthDigest struct { + // Creator is a function returning something that implements hash.Hash interface. + // Creator is required whenever Generator is nil. + Creator func() hash.Hash + // Generator is a function returning an HMAC for a given set of key and plain bytes. + // Generator is optional, but it takes precedence over Creator. + Generator func(key, plain []byte) []byte + // Names contains a list of digest names in various notations. + Names []string + // Size is a number of bytes all computed HMACs will have. + Size int +} + +// HMACGenerateOnClient computes an HMAC of a given plain text with a part of a given StaticKey +// (to be sent by the client to the server). +func (ad *AuthDigest) HMACGenerateOnClient(sk *StaticKey, plain []byte) []byte { + key := sk.GetClientAuthKey(ad.Size) + if ad.Generator != nil { + return ad.Generator(key, plain) + } + return HMACCreateAndGenerate(ad.Creator, key, plain) +} + +// HMACGenerateOnServer computes an HMAC of a given plain text with a part of a given StaticKey +// (to be sent by the server to the client). +func (ad *AuthDigest) HMACGenerateOnServer(sk *StaticKey, plain []byte) []byte { + key := sk.GetServerAuthKey(ad.Size) + if ad.Generator != nil { + return ad.Generator(key, plain) + } + return HMACCreateAndGenerate(ad.Creator, key, plain) +} + +// HMACValidateOnClient compares an expected HMAC (as received by the client from the server) +// with an actual HMAC of a given plain text it computes with a part of a given StaticKey. +func (ad *AuthDigest) HMACValidateOnClient(sk *StaticKey, plain, expected []byte) bool { + actual := ad.HMACGenerateOnServer(sk, plain) + return hmac.Equal(actual, expected) +} + +// HMACValidateOnServer compares an expected HMAC (as received by the server from the client) +// with an actual HMAC of a given plain text it computes with a part of a given StaticKey. +func (ad *AuthDigest) HMACValidateOnServer(sk *StaticKey, plain, expected []byte) bool { + actual := ad.HMACGenerateOnClient(sk, plain) + return hmac.Equal(actual, expected) +} + +// CryptCipher represents a cipher used for en/decrypting control messages. +type CryptCipher struct { + // Decryptor is a function returning plain bytes from encrypted ones. + Decryptor func(key, iv, encrypted []byte) []byte + // Encryptor is a function returning encrypted bytes from plain ones. + Encryptor func(key, iv, plain []byte) []byte + // Names contains a list of cipher names in various notations. + Names []string + // SizeBlock is a number of bytes an initialization vector (IV) must have. + SizeBlock int + // SizeKey is a number of bytes an en/decryption key must have. + SizeKey int +} + +// DecryptOnClient decrypts given encrypted bytes with a part of a given StaticKey +// (as received by the client from the server). +func (cc *CryptCipher) DecryptOnClient(sk *StaticKey, iv, encrypted []byte) []byte { + key := sk.GetClientDecryptKey(cc.SizeKey) + return cc.Decryptor(key, iv, encrypted) +} + +// DecryptOnServer decrypts given encrypted bytes with a part of a given StaticKey +// (as received by the server from the client). +func (cc *CryptCipher) DecryptOnServer(sk *StaticKey, iv, encrypted []byte) []byte { + key := sk.GetServerDecryptKey(cc.SizeKey) + return cc.Decryptor(key, iv, encrypted) +} + +// EncryptOnClient encrypts given plain bytes with a part of a given StaticKey +// (to be sent by the client to the server). +func (cc *CryptCipher) EncryptOnClient(sk *StaticKey, iv, plain []byte) []byte { + key := sk.GetClientEncryptKey(cc.SizeKey) + return cc.Encryptor(key, iv, plain) +} + +// EncryptOnServer encrypts given plain bytes with a part of a given StaticKey +// (to be sent by the server to the client). +func (cc *CryptCipher) EncryptOnServer(sk *StaticKey, iv, plain []byte) []byte { + key := sk.GetServerEncryptKey(cc.SizeKey) + return cc.Encryptor(key, iv, plain) +} + +// StaticKey is an OpenVPN static key used for authentication and encryption of control messages. +// +// Notes: +// +// Authentication. If no key direction is set (i.e. a bidirectional key), OpenVPN uses key[64:64+size] for +// computing and validating HMACs on both the client and the server. If the client has `key-direction 1` +// and the server has `key-direction 0`, OpenVPN uses key[192:192+size] for computing HMACs on the client and +// validating them on the server. If the client has `key-direction 0` and the server has `key-direction 1` +// (i.e. an inverse direction in violation of the recommendations), OpenVPN uses key[64:64+size] for computing +// HMACs on the client and validating them on the server. Inverse and Bidi are mutually exclusive. If both are +// set, Bidi takes precedence. +// +// En/decryption. OpenVPN always takes 2 different keys for encryption and decryption, so Bidi is completely +// ignored. Unless Inverse is set, key[128:128+size] is used for encryption on the client and key[0:0+size] is +// used for decryption on the client with the server applying these keys in the other way. +type StaticKey struct { + // Bidi mimics `key-direction` omitted for both the server and the client. + Bidi bool + // Inverse mimics `key-direction 1` set for the server and `key-direction 0` set for the client. + Inverse bool + // KeyBytes must contain 128 or 256 bytes of the static key. + KeyBytes []byte +} + +// FromBase64 fills sk's KeyBytes from a given base64 string. +func (sk *StaticKey) FromBase64(s string) (err error) { + sk.KeyBytes, err = base64.StdEncoding.DecodeString(s) + return +} + +// FromGroupKeyFile fills sk's KeyBytes from a given group key file. +func (sk *StaticKey) FromGroupKeyFile(path string) error { + return sk.FromFile(path, StaticKeyFromFileHex, StaticKeyBytesTotal*2, sk.FromHex) +} + +// FromServerKeyFile fills sk's KeyBytes from a given server key file. +func (sk *StaticKey) FromServerKeyFile(path string) error { + return sk.FromFile(path, StaticKeyFromFileBase64, base64.StdEncoding.EncodedLen(StaticKeyBytesHalf), sk.FromBase64) +} + +// FromFile fills sk's KeyBytes from a given file. +func (sk *StaticKey) FromFile(path string, re *regexp.Regexp, size int, from func(string) error) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + + n := 1024 + buf := make([]byte, n) + n, err = file.Read(buf) + if err != nil && !errors.Is(err, io.EOF) { + return err + } + + if n > 0 { + var s string + if r := re.FindStringSubmatch(string(buf[:n])); r != nil && len(r) == 2 { + s = strings.ReplaceAll(r[1], "\r", "") + s = strings.ReplaceAll(s, "\n", "") + if size == 0 || len(s) == size { + return from(s) + } + } + } + + return ErrInvalidStaticKeyFileContents +} + +// FromHex fills sk's KeyBytes from a given hex string. +func (sk *StaticKey) FromHex(s string) (err error) { + sk.KeyBytes, err = hex.DecodeString(s) + return +} + +// GetClientAuthBytes returns a quarter of KeyBytes to be used for authentication of control messages +// composed by the client. +func (sk *StaticKey) GetClientAuthBytes() []byte { + if sk.Inverse || sk.Bidi { + return sk.GetQuarterBytes(1) + } + return sk.GetQuarterBytes(3) +} + +// GetClientAuthKey returns a key of a given size from GetClientAuthBytes. +func (sk *StaticKey) GetClientAuthKey(size int) []byte { + return sk.GetClientAuthBytes()[:min(size, StaticKeyBytesQuarter)] +} + +// GetClientEncryptBytes returns a quarter of KeyBytes to be used for encryption of control messages +// composed by the client. +func (sk *StaticKey) GetClientEncryptBytes() []byte { + if sk.Inverse { + return sk.GetQuarterBytes(0) + } + return sk.GetQuarterBytes(2) +} + +// GetClientEncryptKey returns a key of a given size from GetClientEncryptBytes. +func (sk *StaticKey) GetClientEncryptKey(size int) []byte { + return sk.GetClientEncryptBytes()[:min(size, StaticKeyBytesQuarter)] +} + +// GetClientDecryptBytes returns a quarter of KeyBytes to be used for decryption of control messages +// received by the client. +func (sk *StaticKey) GetClientDecryptBytes() []byte { + if sk.Inverse { + return sk.GetQuarterBytes(2) + } + return sk.GetQuarterBytes(0) +} + +// GetClientDecryptKey returns a key of a given size from GetClientDecryptBytes. +func (sk *StaticKey) GetClientDecryptKey(size int) []byte { + return sk.GetClientDecryptBytes()[:min(size, StaticKeyBytesQuarter)] +} + +// GetServerAuthBytes returns a quarter of KeyBytes to be used for authentication of control messages +// composed by the server. +func (sk *StaticKey) GetServerAuthBytes() []byte { + if sk.Inverse && !sk.Bidi { + return sk.GetQuarterBytes(3) + } + return sk.GetQuarterBytes(1) +} + +// GetServerAuthKey returns a key of a given size from GetServerAuthKey. +func (sk *StaticKey) GetServerAuthKey(size int) []byte { + return sk.GetServerAuthBytes()[:min(size, StaticKeyBytesQuarter)] +} + +// GetServerEncryptBytes returns a quarter of KeyBytes to be used for encryption of control messages +// composed by the server. +func (sk *StaticKey) GetServerEncryptBytes() []byte { + return sk.GetClientDecryptBytes() +} + +// GetServerEncryptKey returns a key of a given size from GetServerEncryptBytes. +func (sk *StaticKey) GetServerEncryptKey(size int) []byte { + return sk.GetClientDecryptKey(size) +} + +// GetServerDecryptBytes returns a quarter of KeyBytes to be used for decryption of control messages +// received by the server. +func (sk *StaticKey) GetServerDecryptBytes() []byte { + return sk.GetClientEncryptBytes() +} + +// GetServerDecryptKey returns a key of a given size from GetServerDecryptBytes. +func (sk *StaticKey) GetServerDecryptKey(size int) []byte { + return sk.GetClientEncryptKey(size) +} + +// GetQuarterBytes returns a nth (0-based) quarter of KeyBytes +func (sk *StaticKey) GetQuarterBytes(q uint) []byte { + q = q % 4 + if len(sk.KeyBytes) < StaticKeyBytesTotal { + q = q % 2 + } + if len(sk.KeyBytes) < StaticKeyBytesHalf { + q = 0 + } + if len(sk.KeyBytes) < StaticKeyBytesQuarter { + return sk.KeyBytes + } + return sk.KeyBytes[q*StaticKeyBytesQuarter : (q+1)*StaticKeyBytesQuarter] +} + +// ToBase64 returns a base64 string representing KeyBytes. +func (sk *StaticKey) ToBase64() string { + return base64.StdEncoding.EncodeToString(sk.KeyBytes) +} + +// ToHex returns a hex string representing KeyBytes. +func (sk *StaticKey) ToHex() string { + return hex.EncodeToString(sk.KeyBytes) +} + +// AuthDigests contains all the supported items of AuthDigest type. +var AuthDigests = []*AuthDigest{ + // Legacy digests + {Creator: md5.New, Names: []string{"MD5", "SSL3-MD5", "md5", "ssl3-md5"}, Size: md5.Size}, + {Creator: sha1.New, Names: []string{"SHA-1", "SHA1", "SSL3-SHA1", "sha-1", "sha1", "ssl3-sha1"}, Size: sha1.Size}, + {Creator: ripemd160.New, Names: []string{"RIPEMD-160", "RMD-160", "RIPEMD160", "RIPEMD", "RMD160", "ripemd-160", "rmd-160", "ripemd160", "ripemd", "rmd160"}, Size: ripemd160.Size}, + // SHA2 digests + {Creator: sha256.New224, Names: []string{"SHA-224", "SHA2-224", "SHA224", "sha-224", "sha2-224", "sha224"}, Size: sha256.Size224}, + {Creator: sha256.New, Names: []string{"SHA-256", "SHA2-256", "SHA256", "sha-256", "sha2-256", "sha256"}, Size: sha256.Size}, + {Creator: sha512.New384, Names: []string{"SHA-384", "SHA2-384", "SHA384", "sha-384", "sha2-384", "sha384"}, Size: sha512.Size384}, + {Creator: sha512.New, Names: []string{"SHA-512", "SHA2-512", "SHA512", "sha-512", "sha2-512", "sha512"}, Size: sha512.Size}, + {Creator: sha512.New512_224, Names: []string{"SHA-512/224", "SHA2-512/224", "SHA512-224", "sha-512/224", "sha2-512/224", "sha512-224"}, Size: sha512.Size224}, + {Creator: sha512.New512_256, Names: []string{"SHA-512/256", "SHA2-512/256", "SHA512-256", "sha-512/256", "sha2-512/256", "sha512-256"}, Size: sha512.Size256}, + // SHA3 digests + {Creator: sha3.New224, Names: []string{"SHA3-224", "sha3-224"}, Size: 28}, + {Creator: sha3.New256, Names: []string{"SHA3-256", "sha3-256"}, Size: 32}, + {Creator: sha3.New384, Names: []string{"SHA3-384", "sha3-384"}, Size: 48}, + {Creator: sha3.New512, Names: []string{"SHA3-512", "sha3-512"}, Size: 64}, + // BLAKE digests + { + Creator: func() hash.Hash { + h, _ := blake2s.New256(nil) + return h + }, + Names: []string{"BLAKE2s-256", "BLAKE2S-256", "blake2s-256", "blake2S-256"}, + Size: blake2s.Size, + }, + { + Creator: func() hash.Hash { + h, _ := blake2b.New512(nil) + return h + }, + Names: []string{"BLAKE2b-512", "BLAKE2B-512", "blake2b-512", "blake2B-512"}, + Size: blake2b.Size, + }, + // SHAKE digests + { + Creator: func() hash.Hash { + return sha3.NewShake128() + }, + Names: []string{"SHAKE-128", "SHAKE128", "shake-128", "shake128"}, + Size: 32, + }, + { + Creator: func() hash.Hash { + return sha3.NewShake256() + }, + Names: []string{"SHAKE-256", "SHAKE256", "shake-256", "shake256"}, + Size: 64, + }, + // Custom digests + { + // This MD5-SHA1 implementation outputs bytes that match many online generators. However, + // its output never matches the HMACs of the sample packets generated on Windows and macOS. + // Since OpenVPN uses the OpenSSL library under the hood, the reason for this hash mismatch + // should most likely be traced there. Assuming the MD5-SHA1 digest has a very limited use, + // there is little probability anyone will ever face this issue. + Generator: func(key, plain []byte) []byte { + hmacMD5 := hmac.New(md5.New, key[:md5.Size]) + hmacMD5.Write(plain) + hmacSHA1 := hmac.New(sha1.New, key[:sha1.Size]) + hmacSHA1.Write(plain) + result := make([]byte, 0, md5.Size+sha1.Size) + return hmacSHA1.Sum(hmacMD5.Sum(result)) + }, + Names: []string{"MD5+SHA1", "MD5-SHA1", "MD5SHA1", "md5+sha1", "md5-sha1", "md5sha1"}, + Size: md5.Size + sha1.Size, + }, +} + +// AuthDigestSizes contains sizes of all the supported items of AuthDigest type. +var AuthDigestSizes = func() []int { + presence := make([]bool, AuthHMACBytesMax+1) + n := 0 + for _, ad := range AuthDigests { + if !presence[ad.Size] { + n++ + presence[ad.Size] = true + } + } + sizes := make([]int, 0, n) + for i, present := range presence { + if present { + sizes = append(sizes, i) + } + } + return sizes +}() + +// CryptCiphers contains all the supported items of CryptCipher type. +var CryptCiphers = []*CryptCipher{ + { + Decryptor: func(key, iv, encrypted []byte) []byte { + plain := make([]byte, len(encrypted)) + block, _ := aes.NewCipher(key) + ctr := cipher.NewCTR(block, iv) + ctr.XORKeyStream(plain, encrypted) + return plain + }, + Encryptor: func(key, iv, plain []byte) []byte { + encrypted := make([]byte, len(plain)) + block, _ := aes.NewCipher(key) + ctr := cipher.NewCTR(block, iv) + ctr.XORKeyStream(encrypted, plain) + return encrypted + }, + Names: []string{"AES-256-CTR", "aes-256-ctr"}, + SizeBlock: 16, + SizeKey: 32, + }, +} + +// AuthDigestDefault is the default AuthDigest used in the crypt and crypt2 modes (SHA-256). +var AuthDigestDefault = AuthDigestFindByName("SHA-256") + +// CryptCipherDefault is the default CryptCipher used in the crypt and crypt2 modes (AES-256-CTR). +var CryptCipherDefault = CryptCipherFindByName("AES-256-CTR") + +var StaticKeyFromFileBase64 = regexp.MustCompile("^(?:#.*?\\r?\\n)*" + + "-----BEGIN OpenVPN tls-crypt-v2 (?:client|server) key-----\\r?\\n" + + "([0-9a-zA-Z+=\\/\\r\\n]+)" + + "-----END OpenVPN tls-crypt-v2 (?:client|server) key-----(?:\\r?\\n)?$") +var StaticKeyFromFileHex = regexp.MustCompile("^(?:#.*?\\r?\\n)*" + + "-----BEGIN OpenVPN Static key V1-----\\r?\\n" + + "([0-9a-fA-F\\r\\n]+)" + + "-----END OpenVPN Static key V1-----(?:\\r?\\n)?$") + +var ( + ErrInvalidStaticKeyFileContents = errors.New("invalid static key file contents") +) + +const ( + AuthHMACBytesMax = sha512.Size + AuthHMACBytesMin = md5.Size + + CryptHMACBytesTotal = sha256.Size + + StaticKeyBytesTotal = 256 + StaticKeyBytesHalf = StaticKeyBytesTotal / 2 + StaticKeyBytesQuarter = StaticKeyBytesTotal / 4 +) + +// AuthDigestFindByName returns a pointer to AuthDigest having a given name or nil. +func AuthDigestFindByName(name string) *AuthDigest { + for _, ad := range AuthDigests { + if slices.Contains(ad.Names, name) { + return ad + } + } + return nil +} + +// CryptCipherFindByName returns a pointer to CryptCipher having a given name or nil. +func CryptCipherFindByName(name string) *CryptCipher { + for _, cc := range CryptCiphers { + if slices.Contains(cc.Names, name) { + return cc + } + } + return nil +} + +// HMACCreateAndGenerate uses a given digest creator to compute an HMAC of a given plain text with a given key. +func HMACCreateAndGenerate(creator func() hash.Hash, key, plain []byte) []byte { + hmacDigest := hmac.New(creator, key) + hmacDigest.Write(plain) + return hmacDigest.Sum(nil) +} + +// StaticKeyNewFromBase64 returns a pointer to StaticKey filled with bytes from a given base64 string. +func StaticKeyNewFromBase64(s string, inverse bool, bidi bool) *StaticKey { + sk := &StaticKey{Inverse: inverse, Bidi: bidi} + _ = sk.FromBase64(s) + return sk +} + +// StaticKeyNewFromHex returns a pointer to StaticKey filled with bytes from a given hex string. +func StaticKeyNewFromHex(s string, inverse bool, bidi bool) *StaticKey { + sk := &StaticKey{Inverse: inverse, Bidi: bidi} + _ = sk.FromHex(s) + return sk +} diff --git a/modules/l4openvpn/matcher.go b/modules/l4openvpn/matcher.go new file mode 100644 index 0000000..d28066d --- /dev/null +++ b/modules/l4openvpn/matcher.go @@ -0,0 +1,633 @@ +// Copyright 2024 VNXME +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package l4openvpn + +import ( + "encoding/binary" + "errors" + "io" + "net" + "strings" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + + "github.com/mholt/caddy-l4/layer4" +) + +func init() { + caddy.RegisterModule(&MatchOpenVPN{}) +} + +// MatchOpenVPN is able to match OpenVPN connections. +type MatchOpenVPN struct { + + // Modes contains a list of supported OpenVPN modes to match against incoming client reset messages: + // + // - `plain` mode messages have no replay protection, authentication or encryption; + // + // - `auth` mode messages have no encryption, but provide for replay protection and authentication + // with a pre-shared 2048-bit group key, a variable key direction, and plenty digest algorithms; + // + // - 'crypt' mode messages feature replay protection, authentication and encryption with + // a pre-shared 2048-bit group key, a fixed key direction, and SHA-256 + AES-256-CTR algorithms; + // + // - `crypt2` mode messages are essentially `crypt` messages with an individual 2048-bit client key + // used for authentication and encryption attached to client reset messages in a protected form + // (a 1024-bit server key is used for its authentication end encryption). + // + // Notes: Each mode shall only be present once in the list. Values in the list are case-insensitive. + // If the list is empty, MatchOpenVPN will consider all modes as accepted and try them one by one. + Modes []string `json:"modes,omitempty"` + + /* + * Fields relevant to the auth, crypt and crypt2 modes: + */ + + // IgnoreCrypto makes MatchOpenVPN skip decryption and authentication if set to true. + // + // Notes: IgnoreCrypto impacts the auth, crypt and crypt2 modes at once and makes sense only if/when + // the relevant static keys are provided. If neither GroupKey nor GroupKeyFile is set, decryption + // (if applicable) and authentication are automatically skipped in the auth and crypt modes only. If + // neither ServerKey nor ServerKeyFile is provided, decryption and authentication are automatically + // skipped in the crypt2 mode (unless there is a client key). If neither ClientKeys nor ClientKeyFiles + // are provided, decryption and authentication are automatically skipped in the crypt2 mode (unless + // there is a server key). In the crypt2 mode, when there is a client key and there is no server key, + // decryption of a WrappedKey is impossible, and this part of the incoming message is authenticated by + // comparing it with what has been included in the matching client key. + IgnoreCrypto bool `json:"ignore_crypto,omitempty"` + // IgnoreTimestamp makes MatchOpenVPN skip replay timestamps validation if set to true. + // + // Note: A 30-seconds time window is applicable by default, i.e. a timestamp of up to 15 seconds behind + // or ahead of now is accepted. + IgnoreTimestamp bool `json:"ignore_timestamp,omitempty"` + + /* + * Fields relevant to the auth and crypt modes: + */ + + // GroupKey contains a hex string representing a pre-shared 2048-bit group key. This key may be + // present in OpenVPN config files inside `` or `` blocks or generated with + // `openvpn --genkey tls-auth|tls-crypt` command. No comments (starting with '#' or '-') are allowed. + GroupKey string `json:"group_key,omitempty"` + // GroupKeyFile is a path to a file containing a pre-shared 2048-bit group key which may be present + // in OpenVPN config files after `tls-auth` or `tls-crypt` directives. It is the same key as the one + // GroupKey introduces, so these fields are mutually exclusive. If both are set, GroupKey always takes + // precedence. Any comments in the file (starting with '#' or '-') are ignored. + GroupKeyFile string `json:"group_key_file,omitempty"` + + /* + * Fields relevant to the auth mode only: + */ + + // AuthDigest is a name of a digest algorithm used for authentication (HMAC generation and validation) of + // the auth mode messages. If no value is provided, MatchOpenVPN will try all the algorithms it supports. + // + // Notes: OpenVPN binaries may support a larger number of digest algorithms thanks to the OpenSSL library + // used under the hood. A few legacy and exotic digest algorithms are known to be missing, so IgnoreCrypto + // may be set to true to ensure successful message matching if a desired digest algorithm isn't listed below. + // + // List of the supported digest algorithms: + // - MD5 + // - SHA-1 + // - RIPEMD-160 + // - SHA-224 + // - SHA-256 + // - SHA-384 + // - SHA-512 + // - SHA-512/224 + // - SHA-512/256 + // - SHA3-224 + // - SHA3-256 + // - SHA3-384 + // - SHA3-512 + // - BLAKE2s-256 + // - BLAKE2b-512 + // - SHAKE-128 + // - SHAKE-256 + // + // Note: Digest algorithm names are recognised in a number of popular notations, including lowercase. + // Please, refer to the source code (AuthDigests variable in crypto.go) for details. + AuthDigest string `json:"auth_digest,omitempty"` + // GroupKeyDirection is a group key direction and may contain one of the following three values: + // + // - `normal` means the server config has `tls-auth [...] 0` or `key-direction 0`, + // while the client configs have `tls-auth [...] 1` or `key-direction 1`; + // + // - `inverse` means the server config has `tls-auth [...] 1` or `key-direction 1`, + // while the client config have `tls-auth [...] 0` or `key-direction 0`; + // + // - `bidi` or `bidirectional` means key direction is omitted (e.g. `tls-auth [...]`) + // in both the server config and client configs. + // + // Notes: Values are case-insensitive. If no value is specified, the normal key direction is implied. + // The inverse key direction is a violation of the OpenVPN official recommendations, and the bidi one + // provides for a lower level of DoS and message replay attacks resilience. + GroupKeyDirection string `json:"group_key_direction,omitempty"` + + /* + * Fields relevant to the crypt2 mode only: + */ + + // ClientKeys contains a list of base64 strings representing 2048-bit client keys (each one in a decrypted + // form followed by an encrypted and authenticated form also known as WKc in the OpenVPN docs). These keys + // may be present in OpenVPN client config files inside `` block or generated with `openvpn + // --tls-crypt-v2 [server.key] --genkey tls-crypt-v2-client` command. No comments (starting with '#' or '-') + // are allowed. + ClientKeys []string `json:"client_keys,omitempty"` + // ClientKeyFiles is a list of paths to files containing 2048-bit client key which may be present in OpenVPN + // config files after `tls-crypt-v2` directive. These are the same keys as those ClientKeys introduce, but + // these fields are complementary. If both are set, a joint list of client keys is created. Any comments in + // the files (starting with '#' or '-') are ignored. + ClientKeyFiles []string `json:"client_key_files,omitempty"` + + // ServerKey contains a base64 string representing a 1024-bit server key used only for authentication and + // encryption of client keys. This key may be present in OpenVPN server config files inside `` + // block or generated with `openvpn --genkey tls-crypt-v2-server` command. No comments (starting with '#' + // or '-') are allowed. + ServerKey string `json:"server_key,omitempty"` + // ServerKeyFile is a path to a file containing a 1024-bit server key which may be present in OpenVPN + // config files after `tls-crypt-v2` directive. It is the same key as the one ServerKey introduces, so + // these fields are mutually exclusive. If both are set, ServerKey always takes precedence. Any comments + // in the file (starting with '#' or '-') are ignored. + ServerKeyFile string `json:"server_key_file,omitempty"` + + /* + * Internal fields: + */ + + acceptAuth bool + acceptCrypt bool + acceptCrypt2 bool + acceptPlain bool + + groupKeyAuth *StaticKey + groupKeyCrypt *StaticKey + + authDigest *AuthDigest + lastDigest *AuthDigest + + clientKeys []*WrappedKey + serverKey *StaticKey +} + +// CaddyModule returns the Caddy module information. +func (m *MatchOpenVPN) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "layer4.matchers.openvpn", + New: func() caddy.Module { return new(MatchOpenVPN) }, + } +} + +// Match returns true if the connection looks like OpenVPN. +func (m *MatchOpenVPN) Match(cx *layer4.Connection) (bool, error) { + var err error + var l, n int + + // Prepare a 3-byte buffer + buf := make([]byte, LengthBytesTotal+OpcodeKeyIDBytesTotal) + + // Do TCP-specific reads and checks + _, isTCP := cx.LocalAddr().(*net.TCPAddr) + if isTCP { + // Read 2 bytes containing the remaining bytes length + _, err = io.ReadFull(cx, buf[:LengthBytesTotal]) + if err != nil { + return false, err + } + + // Validate the remaining bytes length + l = int(binary.BigEndian.Uint16(buf[:LengthBytesTotal])) + if l < MessagePlainBytesTotal || l > MessageCrypt2BytesMax { + return false, nil + } + } + + // Read 1 byte containing MessageHeader + _, err = io.ReadFull(cx, buf[LengthBytesTotal:]) + if err != nil { + return false, err + } + + // Parse MessageHeader + hdr := &MessageHeader{} + if err = hdr.FromBytes(buf[LengthBytesTotal:]); err != nil { + return false, nil + } + + // Validate MessageHeader.KeyID + if hdr.KeyID > 0 { + return false, nil + } + + var mp *MessagePlain + var ma *MessageAuth + var mc *MessageCrypt + var mr *MessageCrypt2 + + if hdr.Opcode == OpcodeControlHardResetClientV2 && (m.acceptPlain || m.acceptAuth || m.acceptCrypt) { + if isTCP { + if l > MessageAuthBytesMax { + return false, nil + } + + buf = make([]byte, l-OpcodeKeyIDBytesTotal+1) + n, err = io.ReadAtLeast(cx, buf, l-OpcodeKeyIDBytesTotal) + if err != nil || n > l-OpcodeKeyIDBytesTotal { + return false, err + } + } else { + buf = make([]byte, MessageAuthBytesMaxHL+1) + n, err = io.ReadAtLeast(cx, buf, 1) + if err != nil || n < MessagePlainBytesTotalHL || n > MessageAuthBytesMaxHL { + return false, err + } + } + + if m.acceptPlain { + // Parse and validate MessagePlain + mp = &MessagePlain{} + err = mp.FromBytesHeadless(buf[:n], hdr) + if err == nil && mp.Match() { + return true, nil + } + } + + if m.acceptAuth { + // Parse and validate MessageAuth + ma = &MessageAuth{MessageTraitAuth: MessageTraitAuth{Digest: m.lastDigest}} + err = ma.FromBytesHeadless(buf[:n], hdr) + if err == nil && ma.Match(m.IgnoreTimestamp, m.IgnoreCrypto, m.authDigest, m.groupKeyAuth) { + m.lastDigest = ma.Digest + return true, nil + } + } + + if m.acceptCrypt { + // Parse and validate MessageCrypt + mc = &MessageCrypt{} + err = mc.FromBytesHeadless(buf[:n], hdr) + if err == nil && mc.Match(m.IgnoreTimestamp, m.IgnoreCrypto, nil, m.groupKeyCrypt) { + return true, nil + } + } + } + + if hdr.Opcode == OpcodeControlHardResetClientV3 && m.acceptCrypt2 { + if isTCP { + if l < MessageCrypt2BytesMin { + return false, nil + } + + buf = make([]byte, l-OpcodeKeyIDBytesTotal+1) + n, err = io.ReadAtLeast(cx, buf, l-OpcodeKeyIDBytesTotal) + if err != nil || n > l-OpcodeKeyIDBytesTotal { + return false, err + } + } else { + buf = make([]byte, MessageCrypt2BytesMaxHL+1) + n, err = io.ReadAtLeast(cx, buf, 1) + if err != nil || n < MessageCrypt2BytesMinHL || n > MessageCrypt2BytesMaxHL { + return false, err + } + } + + // Parse and validate MessageCrypt2 + mr = &MessageCrypt2{} + err = mr.FromBytesHeadless(buf[:n], hdr) + if err == nil && mr.Match(m.IgnoreTimestamp, m.IgnoreCrypto, nil, m.serverKey, m.clientKeys) { + return true, nil + } + } + + return false, nil +} + +// Provision prepares m's internal structures. +func (m *MatchOpenVPN) Provision(_ caddy.Context) error { + repl := caddy.NewReplacer() + + if len(m.Modes) > 0 { + for _, mode := range m.Modes { + mode = strings.ToLower(repl.ReplaceAll(mode, "")) + switch mode { + case ModeAuth: + m.acceptAuth = true + case ModeCrypt: + m.acceptCrypt = true + case ModeCrypt2: + m.acceptCrypt2 = true + case ModePlain: + m.acceptPlain = true + default: + return ErrInvalidMode + } + } + } else { + m.acceptAuth, m.acceptCrypt, m.acceptCrypt2, m.acceptPlain = true, true, true, true + } + + var gkdBidi, gkdInverse bool + m.GroupKeyDirection = strings.ToLower(repl.ReplaceAll(m.GroupKeyDirection, "")) + if len(m.GroupKeyDirection) > 0 { + switch m.GroupKeyDirection { + case GroupKeyDirectionBidi, GroupKeyDirectionBidi2: + gkdBidi = true + case GroupKeyDirectionInverse: + gkdInverse = true + case GroupKeyDirectionNormal: + break + default: + return ErrInvalidGroupKeyDirection + } + } + + m.GroupKey = repl.ReplaceAll(m.GroupKey, "") + if len(m.GroupKey) > 0 { + sk := &StaticKey{} + if err := sk.FromHex(m.GroupKey); err != nil { + return err + } + if len(sk.KeyBytes) != StaticKeyBytesTotal { + return ErrInvalidGroupKey + } + m.groupKeyAuth, m.groupKeyCrypt = &StaticKey{Bidi: gkdBidi, Inverse: gkdInverse, KeyBytes: sk.KeyBytes}, sk + } else { + m.GroupKeyFile = repl.ReplaceAll(m.GroupKeyFile, "") + if len(m.GroupKeyFile) > 0 { + sk := &StaticKey{} + if err := sk.FromGroupKeyFile(m.GroupKeyFile); err != nil { + return err + } + if len(sk.KeyBytes) != StaticKeyBytesTotal { + return ErrInvalidGroupKey + } + m.groupKeyAuth, m.groupKeyCrypt = &StaticKey{Bidi: gkdBidi, Inverse: gkdInverse, KeyBytes: sk.KeyBytes}, sk + } + } + + m.AuthDigest = repl.ReplaceAll(m.AuthDigest, "") + if len(m.AuthDigest) > 0 { + m.authDigest = AuthDigestFindByName(m.AuthDigest) + if m.authDigest == nil { + return ErrInvalidAuthDigest + } + } + + m.ServerKey = repl.ReplaceAll(m.ServerKey, "") + if len(m.ServerKey) > 0 { + sk := &StaticKey{} + if err := sk.FromBase64(m.ServerKey); err != nil { + return err + } + if len(sk.KeyBytes) != StaticKeyBytesHalf { + return ErrInvalidServerKey + } + m.serverKey = sk + } else { + m.ServerKeyFile = repl.ReplaceAll(m.ServerKeyFile, "") + if len(m.ServerKeyFile) > 0 { + sk := &StaticKey{} + if err := sk.FromServerKeyFile(m.ServerKeyFile); err != nil { + return err + } + if len(sk.KeyBytes) != StaticKeyBytesHalf { + return ErrInvalidServerKey + } + m.serverKey = sk + } + } + + if len(m.ClientKeys) > 0 { + for _, clientKey := range m.ClientKeys { + clientKey = repl.ReplaceAll(clientKey, "") + if len(clientKey) > 0 { + ck := &WrappedKey{} + if err := ck.FromBase64(clientKey); err != nil { + return err + } + + if len(ck.StaticKey.KeyBytes) != StaticKeyBytesTotal || + (m.serverKey != nil && !ck.DecryptAndAuthenticate(nil, m.serverKey)) { + return ErrInvalidClientKey + } + + m.clientKeys = append(m.clientKeys, ck) + } + } + } else if len(m.ClientKeyFiles) > 0 { + for _, clientKeyFile := range m.ClientKeyFiles { + clientKeyFile = repl.ReplaceAll(clientKeyFile, "") + if len(clientKeyFile) > 0 { + ck := &WrappedKey{} + if err := ck.FromClientKeyFile(clientKeyFile); err != nil { + return err + } + + if len(ck.StaticKey.KeyBytes) != StaticKeyBytesTotal || + (m.serverKey != nil && !ck.DecryptAndAuthenticate(nil, m.serverKey)) { + return ErrInvalidClientKey + } + + m.clientKeys = append(m.clientKeys, ck) + } + } + } + + return nil +} + +// UnmarshalCaddyfile sets up the MatchOpenVPN from Caddyfile tokens. Syntax: +// +// openvpn { +// modes [<...>] +// +// ignore_crypto +// ignore_timestamp +// +// group_key +// group_key_file +// +// auth_digest +// group_key_direction +// +// server_key +// server_key_file +// +// client_key +// client_key_file +// } +// openvpn +// +// Note: multiple 'client_key' and 'client_key_file' options are allowed. +func (m *MatchOpenVPN) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + _, wrapper := d.Next(), d.Val() // consume wrapper name + + // No same-line arguments are supported + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + + errDuplicate := func(optionName string) error { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + + errGroupKeyMutex := func() error { + return d.Errf("%s options 'group_key' and `group_key_file` are mutually exclusive", wrapper) + } + + errServerKeyMutex := func() error { + return d.Errf("%s options 'server_key' and `server_key_file` are mutually exclusive", wrapper) + } + + var hasAuthDigest, hasGroupKey, hasGroupKeyDirection, hasGroupKeyFile, hasIgnoreCrypto, hasIgnoreTimestamp, + hasModes, hasServerKey, hasServerKeyFile bool + for nesting := d.Nesting(); d.NextBlock(nesting); { + optionName := d.Val() + switch optionName { + case "modes": + if hasModes { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 4 { + return d.ArgErr() + } + m.Modes, hasModes = append(m.Modes, d.RemainingArgs()...), true + case "ignore_crypto": + if hasIgnoreCrypto { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + m.IgnoreCrypto, hasIgnoreCrypto = true, true + case "ignore_timestamp": + if hasIgnoreTimestamp { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + m.IgnoreTimestamp, hasIgnoreTimestamp = true, true + case "group_key": + if hasGroupKeyFile { + return errGroupKeyMutex() + } + if hasGroupKey { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, m.GroupKey, hasGroupKey = d.NextArg(), d.Val(), true + case "group_key_file": + if hasGroupKey { + return errGroupKeyMutex() + } + if hasGroupKeyFile { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, m.GroupKeyFile, hasGroupKeyFile = d.NextArg(), d.Val(), true + case "auth_digest": + if hasAuthDigest { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, m.AuthDigest, hasAuthDigest = d.NextArg(), d.Val(), true + case "group_key_direction": + if hasGroupKeyDirection { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, m.GroupKeyDirection, hasGroupKeyDirection = d.NextArg(), d.Val(), true + case "server_key": + if hasServerKeyFile { + return errServerKeyMutex() + } + if hasServerKey { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, m.ServerKey, hasServerKey = d.NextArg(), d.Val(), true + case "server_key_file": + if hasServerKey { + return errServerKeyMutex() + } + if hasServerKeyFile { + return errDuplicate(optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, m.ServerKeyFile, hasServerKeyFile = d.NextArg(), d.Val(), true + case "client_key": + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + m.ClientKeys = append(m.ClientKeys, d.RemainingArgs()...) + case "client_key_file": + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + m.ClientKeyFiles = append(m.ClientKeyFiles, d.RemainingArgs()...) + default: + return d.ArgErr() + } + + // No nested blocks are supported + if d.NextBlock(nesting + 1) { + return d.Errf("malformed %s option '%s': nested blocks are not supported", wrapper, optionName) + } + } + + return nil +} + +// Interface guards +var ( + _ caddy.Provisioner = (*MatchOpenVPN)(nil) + _ caddyfile.Unmarshaler = (*MatchOpenVPN)(nil) + _ layer4.ConnMatcher = (*MatchOpenVPN)(nil) +) + +var ( + ErrInvalidAuthDigest = errors.New("invalid auth digest") + ErrInvalidClientKey = errors.New("invalid client key") + ErrInvalidGroupKey = errors.New("invalid group key") + ErrInvalidGroupKeyDirection = errors.New("invalid group key direction") + ErrInvalidMode = errors.New("invalid mode") + ErrInvalidServerKey = errors.New("invalid server key") +) + +const ( + GroupKeyDirectionBidi = "bidi" + GroupKeyDirectionBidi2 = "bidirectional" + GroupKeyDirectionInverse = "inverse" + GroupKeyDirectionNormal = "normal" + + ModeAuth = "auth" + ModeCrypt = "crypt" + ModeCrypt2 = "crypt2" + ModePlain = "plain" +) diff --git a/modules/l4openvpn/matcher_test.go b/modules/l4openvpn/matcher_test.go new file mode 100644 index 0000000..bc617ee --- /dev/null +++ b/modules/l4openvpn/matcher_test.go @@ -0,0 +1,551 @@ +// Copyright 2024 VNXME +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package l4openvpn + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net" + "slices" + "testing" + + "github.com/caddyserver/caddy/v2" + "go.uber.org/zap" + + "github.com/mholt/caddy-l4/layer4" +) + +func assertNoError(t *testing.T, err error) { + t.Helper() + if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { + t.Fatalf("Unexpected error: %s\n", err) + } +} + +func Test_AuthDigests_CheckAll(t *testing.T) { + plain := plainPacket1[9:14] + names := make(map[string]int, len(AuthDigests)) + hashes := make([][]byte, 0, len(AuthDigests)) + var hmac []byte + for i, ad := range AuthDigests { + if len(ad.Names) == 0 { + t.Fatalf("Test %d: there must be at least one name", i) + } + for j, name := range ad.Names { + if len(name) == 0 { + t.Fatalf("Test %d [%s]: empty name %d", i, ad.Names[0], j) + } + if k, existing := names[name]; existing { + t.Fatalf("Test %d [%s]: name %d [%s] used by %s", i, ad.Names[0], j, name, AuthDigests[k].Names[0]) + } + names[name] = i + } + if ad.Size == 0 { + t.Fatalf("Test %d [%s]: zero size", i, ad.Names[0]) + } + if !slices.Contains(AuthDigestSizes, ad.Size) { + t.Fatalf("Test %d [%s]: size missing in AuthDigestSizes", i, ad.Names[0]) + } + if ad.Creator == nil && ad.Generator == nil { + t.Fatalf("Test %d [%s]: missing a creator or a generator", i, ad.Names[0]) + } + if ad.Generator != nil { + hmac = ad.Generator(groupKey12.GetClientAuthKey(ad.Size), plain) + } else { + hmac = HMACCreateAndGenerate(ad.Creator, groupKey12.GetClientAuthKey(ad.Size), plain) + } + if len(hmac) != ad.Size { + t.Fatalf("Test %d [%s]: HMAC length doesn't match its size", i, ad.Names[0]) + } + for j, existing := range hashes { + if bytes.Equal(hmac, existing) { + t.Fatalf("Test %d [%s]: HMAC bytes same as %s", i, ad.Names[0], AuthDigests[j].Names[0]) + } + } + hashes = append(hashes, hmac) + } +} + +func Test_MessagePlain_FromBytes_Match_ToBytes(t *testing.T) { + for i, packet := range [][]byte{ + plainPacket1, plainPacket2, + plainPacket3, plainPacket4, + } { + msg := &MessagePlain{} + if err := msg.FromBytes(packet); err != nil { + t.Fatalf("Test %d: failed to unpack: %s", i, err) + } + if !bytes.Equal(packet, msg.ToBytes()) { + t.Fatalf("Test %d: failed to pack", i) + } + if !msg.Match() { + t.Fatalf("Test %d: failed to match", i) + } + } +} + +func Test_MessageAuth_FromBytes_Match_ToBytes(t *testing.T) { + for i, packet := range [][]byte{ + // Legacy digests + authMD5Packet1, authMD5Packet2, + authSHA1Packet1, authSHA1Packet2, + authRIPEMD160Packet1, authRIPEMD160Packet2, + // SHA2 digests + authSHA224Packet1, authSHA224Packet2, + authSHA256Packet1, authSHA256Packet2, + authSHA384Packet1, authSHA384Packet2, + authSHA512Packet1, authSHA512Packet2, + authSHA512224Packet1, authSHA512224Packet2, + authSHA512256Packet1, authSHA512256Packet2, + // SHA3 digests + authSHA3224Packet1, authSHA3224Packet2, + authSHA3256Packet1, authSHA3256Packet2, + authSHA3384Packet1, authSHA3384Packet2, + authSHA3512Packet1, authSHA3512Packet2, + // BLAKE digests + authBLAKE2s256Packet1, authBLAKE2s256Packet2, + authBLAKE2b512Packet1, authBLAKE2b512Packet2, + } { + msg := &MessageAuth{} + if err := msg.FromBytes(packet); err != nil { + t.Fatalf("Test %d: failed to unpack: %s", i, err) + } + if !msg.Match(true, false, nil, groupKey12) { + t.Fatalf("Test %d: failed to match", i) + } + if !bytes.Equal(packet, msg.ToBytes()) { + t.Fatalf("Test %d: failed to pack", i) + } + } +} + +func Test_MessageAuth_FromBytes_Match(t *testing.T) { + for i, packet := range [][]byte{ + // Legacy digests + authMD5Packet3, authMD5Packet4, + authSHA1Packet3, authSHA1Packet4, + // SHA2 digests + authSHA224Packet3, authSHA224Packet4, + authSHA256Packet3, authSHA256Packet4, + authSHA384Packet3, authSHA384Packet4, + authSHA512Packet3, authSHA512Packet4, + } { + msg := &MessageAuth{} + if err := msg.FromBytes(packet); err != nil { + t.Fatalf("Test %d: failed to unpack: %s", i, err) + } + if !msg.Match(true, true, nil, groupKey12) { + t.Fatalf("Test %d: failed to match", i) + } + } + +} + +func Test_MessageCrypt_FromBytes_Match_ToBytes(t *testing.T) { + for i, packet := range [][]byte{ + cryptPacket1, cryptPacket2, + } { + msg := &MessageCrypt{} + if err := msg.FromBytes(packet); err != nil { + t.Fatalf("Test %d: failed to unpack: %s", i, err) + } + if !msg.Match(true, false, nil, groupKey12) { + t.Fatalf("Test %d: failed to match", i) + } + if !bytes.Equal(packet, msg.ToBytes()) { + t.Fatalf("Test %d: failed to pack", i) + } + } +} + +func Test_MessageCrypt_FromBytes_Match(t *testing.T) { + for i, packet := range [][]byte{ + cryptPacket3, cryptPacket4, + } { + msg := &MessageCrypt{} + if err := msg.FromBytes(packet); err != nil { + t.Fatalf("Test %d: failed to unpack: %s", i, err) + } + if !msg.Match(true, true, nil, groupKey12) { + t.Fatalf("Test %d: failed to match", i) + } + } +} + +func Test_MessageCrypt2_FromBytes_Match_ToBytes(t *testing.T) { + for i, packet := range [][]byte{ + crypt2Packet5, crypt2Packet6, + } { + msg := &MessageCrypt2{} + if err := msg.FromBytes(packet); err != nil { + t.Fatalf("Test %d: failed to unpack: %s", i, err) + } + if !msg.Match(true, false, nil, serverKey56, nil) { + t.Fatalf("Test %d: failed to match with a server key", i) + } + if !msg.Match(true, false, nil, nil, []*WrappedKey{clientKey56}) { + t.Fatalf("Test %d: failed to match with a client key", i) + } + if !bytes.Equal(packet, msg.ToBytes()) { + fmt.Printf("%x\n%x\n", packet, msg.ToBytes()) + t.Fatalf("Test %d: failed to pack", i) + } + } +} + +func Test_MessageCrypt2_FromBytes_Match(t *testing.T) { + for i, packet := range [][]byte{ + crypt2Packet5, crypt2Packet6, + } { + msg := &MessageCrypt2{} + if err := msg.FromBytes(packet); err != nil { + t.Fatalf("Test %d: failed to unpack: %s", i, err) + } + if !msg.Match(true, false, nil, nil, nil) { + t.Fatalf("Test %d: failed to match", i) + } + } +} + +func Test_MatchOpenVPN_Match(t *testing.T) { + type test struct { + matcher *MatchOpenVPN + data []byte + shouldMatch bool + } + + modesNotAuth := []string{"crypt", "crypt2", "plain"} + modesNotPlain := []string{"auth", "crypt", "crypt2"} + modesNotCrypt := []string{"auth", "crypt2", "plain"} + modesNotCrypt2 := []string{"auth", "crypt", "plain"} + + testsPlain := func() []test { + m0 := &MatchOpenVPN{} + m1 := &MatchOpenVPN{Modes: modesNotPlain} + tests := make([]test, 0, 3*2*2) + for i, packet := range [][]byte{ + plainPacket1, plainPacket2, + plainPacket3, plainPacket4, + } { + tests = append(tests, + test{matcher: m0, data: packet[:MessagePlainBytesTotal-i-1], shouldMatch: false}, + test{matcher: m0, data: packet, shouldMatch: true}, + test{matcher: m1, data: packet, shouldMatch: false}, + ) + } + return tests + }() + + testsKnownKeyAuth := func() []test { + m0 := &MatchOpenVPN{} + m1 := &MatchOpenVPN{IgnoreTimestamp: true} + m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotAuth} + m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex} + m4 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex, AuthDigest: "shake128"} + m5 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex, GroupKeyDirection: GroupKeyDirectionInverse} + tests := make([]test, 0, 6*15*2) + for _, packet := range [][]byte{ + authMD5Packet1, authMD5Packet2, + authSHA1Packet1, authSHA1Packet2, + authRIPEMD160Packet1, authRIPEMD160Packet2, + authSHA224Packet1, authSHA224Packet2, + authSHA256Packet1, authSHA256Packet2, + authSHA384Packet1, authSHA384Packet2, + authSHA512Packet1, authSHA512Packet2, + authSHA512224Packet1, authSHA512224Packet2, + authSHA512256Packet1, authSHA512256Packet2, + authSHA3224Packet1, authSHA3224Packet2, + authSHA3256Packet1, authSHA3256Packet2, + authSHA3384Packet1, authSHA3384Packet2, + authSHA3512Packet1, authSHA3512Packet2, + authBLAKE2s256Packet1, authBLAKE2s256Packet2, + authBLAKE2b512Packet1, authBLAKE2b512Packet2, + } { + tests = append(tests, + test{matcher: m0, data: packet, shouldMatch: false}, + test{matcher: m1, data: packet, shouldMatch: true}, + test{matcher: m2, data: packet, shouldMatch: false}, + test{matcher: m3, data: packet, shouldMatch: true}, + test{matcher: m4, data: packet, shouldMatch: false}, + test{matcher: m5, data: packet, shouldMatch: false}, + ) + } + return tests + }() + + testsUnknownKeyAuth := func() []test { + m0 := &MatchOpenVPN{} + m1 := &MatchOpenVPN{IgnoreTimestamp: true} + m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotAuth} + m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex} + tests := make([]test, 0, 4*6*2) + for _, packet := range [][]byte{ + authMD5Packet3, authMD5Packet4, + authSHA1Packet3, authSHA1Packet4, + authSHA224Packet3, authSHA224Packet4, + authSHA256Packet3, authSHA256Packet4, + authSHA384Packet3, authSHA384Packet4, + authSHA512Packet3, authSHA512Packet4, + } { + tests = append(tests, + test{matcher: m0, data: packet, shouldMatch: false}, + test{matcher: m1, data: packet, shouldMatch: true}, + test{matcher: m2, data: packet, shouldMatch: false}, + test{matcher: m3, data: packet, shouldMatch: false}, + ) + } + return tests + }() + + testsUnsupportedDigestsAuth := func() []test { + m0 := &MatchOpenVPN{} + m1 := &MatchOpenVPN{IgnoreTimestamp: true} + m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotAuth} + m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex} + tests := make([]test, 0, 4*6*2) + for _, packet := range [][]byte{ + authMD5SHA1Packet1, authMD5SHA1Packet2, + authSM3Packet1, authSM3Packet2, + authWhirlpoolPacket1, authWhirlpoolPacket2, + authMD5SHA1Packet3, authMD5SHA1Packet4, + } { + tests = append(tests, + test{matcher: m0, data: packet, shouldMatch: false}, + test{matcher: m1, data: packet, shouldMatch: true}, + test{matcher: m2, data: packet, shouldMatch: false}, + test{matcher: m3, data: packet, shouldMatch: false}, + ) + } + return tests + }() + + testsKnownKeyCrypt := func() []test { + m0 := &MatchOpenVPN{} + m1 := &MatchOpenVPN{IgnoreTimestamp: true} + m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotCrypt} + m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex} + tests := make([]test, 0, 4*1*2) + for _, packet := range [][]byte{ + cryptPacket1, cryptPacket2, + } { + tests = append(tests, + test{matcher: m0, data: packet, shouldMatch: false}, + test{matcher: m1, data: packet, shouldMatch: true}, + test{matcher: m2, data: packet, shouldMatch: false}, + test{matcher: m3, data: packet, shouldMatch: true}, + ) + } + return tests + }() + + testsUnknownKeyCrypt := func() []test { + m0 := &MatchOpenVPN{} + m1 := &MatchOpenVPN{IgnoreTimestamp: true} + m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotCrypt} + m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex} + tests := make([]test, 0, 4*1*2) + for _, packet := range [][]byte{ + cryptPacket3, cryptPacket4, + } { + tests = append(tests, + test{matcher: m0, data: packet, shouldMatch: false}, + test{matcher: m1, data: packet, shouldMatch: true}, + test{matcher: m2, data: packet, shouldMatch: false}, + test{matcher: m3, data: packet, shouldMatch: false}, + ) + } + return tests + }() + + testsKnownKeyCrypt2 := func() []test { + m0 := &MatchOpenVPN{} + m1 := &MatchOpenVPN{IgnoreTimestamp: true} + m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotCrypt2} + m3 := &MatchOpenVPN{IgnoreTimestamp: true, ServerKey: serverKey56Base64} + m4 := &MatchOpenVPN{IgnoreTimestamp: true, ClientKeys: []string{clientKey56Base64}} + tests := make([]test, 0, 5*1*2) + for _, packet := range [][]byte{ + crypt2Packet5, crypt2Packet6, + } { + tests = append(tests, + test{matcher: m0, data: packet, shouldMatch: false}, + test{matcher: m1, data: packet, shouldMatch: true}, + test{matcher: m2, data: packet, shouldMatch: false}, + test{matcher: m3, data: packet, shouldMatch: true}, + test{matcher: m4, data: packet, shouldMatch: true}, + ) + } + return tests + }() + + tests := make([]test, 0, len(testsPlain)+len(testsKnownKeyAuth)+len(testsUnknownKeyAuth)+ + len(testsUnsupportedDigestsAuth)+len(testsKnownKeyCrypt)+len(testsUnknownKeyCrypt)+len(testsKnownKeyCrypt2)) + tests = append(tests, testsPlain...) + tests = append(tests, testsKnownKeyAuth...) + tests = append(tests, testsUnknownKeyAuth...) + tests = append(tests, testsUnsupportedDigestsAuth...) + tests = append(tests, testsKnownKeyCrypt...) + tests = append(tests, testsUnknownKeyCrypt...) + tests = append(tests, testsKnownKeyCrypt2...) + + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + for i, tc := range tests { + func() { + err := tc.matcher.Provision(ctx) + assertNoError(t, err) + + in, out := net.Pipe() + defer func() { + _, _ = io.Copy(io.Discard, out) + _ = out.Close() + }() + + cx := layer4.WrapConnection(out, []byte{}, zap.NewNop()) + go func() { + _, err := in.Write(tc.data) + assertNoError(t, err) + _ = in.Close() + }() + + matched, err := tc.matcher.Match(cx) + assertNoError(t, err) + + if matched != tc.shouldMatch { + if tc.shouldMatch { + t.Fatalf("Test %d: matcher did not match | %+v\n", i, tc.matcher) + } else { + t.Fatalf("Test %d: matcher should not match | %+v\n", i, tc.matcher) + } + } + }() + } +} + +// https://github.com/OpenVPN/openvpn/blob/master/sample/sample-keys/ta.key +var groupKey12Hex = "" + + "21d94830510107f8753d3b6f3145e01d" + + "ed37075115afcb0538ecdd8503ee9663" + + "7218c9ed38d908d594231d7d143c73da" + + "5055310f89d336da99c8b3dcb18909c7" + + "9dd44f540670ebc0f120beb7211e9683" + + "9cb542572c48bfa7ffaa9a22cb8304b7" + + "869b92f4442918e598745bb78ac8877f" + + "02b00a7cdef3f2446c130d39a7c45126" + + "9ef399fd6029cdfc80a7c604041312ab" + + "0a969bc906bdee6e6d707afdcbe8c7fb" + + "97beb66049c3d328340775025433ceba" + + "1e38008a826cf92443d903106199373b" + + "dadd9c2c735cf481e580db4e81b99f12" + + "e3f46b6159c687cd1b9e689f7712573c" + + "0f02735a45573dfb5cd55cf464942389" + + "2c7e91f439bdd7337a8ceebd302cfbfa" + +var groupKey12 = StaticKeyNewFromHex(groupKey12Hex, false, false) + +/* + * All the sample packets below are generated with the sample static key above. + */ + +var plainPacket1 = []byte{56, 131, 30, 193, 48, 89, 179, 111, 104, 0, 0, 0, 0, 0} +var plainPacket2 = []byte{56, 48, 212, 183, 154, 72, 13, 92, 194, 0, 0, 0, 0, 0} + +var authMD5Packet1 = []byte{56, 108, 88, 142, 73, 58, 114, 77, 35, 45, 192, 5, 145, 148, 66, 118, 118, 229, 204, 60, 174, 162, 74, 50, 78, 0, 0, 0, 1, 102, 234, 243, 9, 0, 0, 0, 0, 0} +var authMD5Packet2 = []byte{56, 31, 34, 72, 211, 219, 0, 85, 46, 200, 142, 75, 104, 53, 70, 109, 234, 137, 253, 29, 138, 148, 218, 83, 141, 0, 0, 0, 1, 102, 234, 243, 39, 0, 0, 0, 0, 0} +var authSHA1Packet1 = []byte{56, 38, 129, 217, 92, 90, 2, 14, 97, 123, 32, 15, 106, 140, 112, 232, 206, 242, 138, 133, 246, 151, 31, 71, 44, 140, 201, 188, 248, 0, 0, 0, 1, 102, 234, 241, 204, 0, 0, 0, 0, 0} +var authSHA1Packet2 = []byte{56, 200, 170, 60, 164, 170, 196, 13, 56, 240, 33, 30, 131, 14, 244, 151, 16, 1, 7, 173, 226, 133, 237, 132, 58, 101, 188, 6, 132, 0, 0, 0, 1, 102, 234, 242, 139, 0, 0, 0, 0, 0} +var authRIPEMD160Packet1 = []byte{56, 30, 52, 26, 73, 175, 7, 96, 168, 184, 22, 237, 90, 206, 228, 15, 190, 115, 56, 133, 2, 91, 146, 56, 141, 94, 239, 86, 106, 0, 0, 0, 1, 102, 235, 48, 209, 0, 0, 0, 0, 0} +var authRIPEMD160Packet2 = []byte{56, 248, 184, 244, 38, 24, 29, 19, 178, 189, 169, 134, 190, 27, 29, 4, 48, 38, 158, 6, 149, 140, 127, 148, 8, 199, 15, 254, 191, 0, 0, 0, 1, 102, 235, 48, 246, 0, 0, 0, 0, 0} + +var authSHA224Packet1 = []byte{56, 120, 162, 216, 21, 223, 131, 234, 134, 80, 127, 130, 174, 30, 102, 244, 238, 216, 176, 213, 66, 172, 6, 45, 221, 153, 93, 227, 228, 70, 180, 76, 82, 233, 176, 242, 229, 0, 0, 0, 1, 102, 234, 243, 221, 0, 0, 0, 0, 0} +var authSHA224Packet2 = []byte{56, 122, 21, 55, 125, 40, 237, 190, 189, 15, 86, 80, 48, 61, 30, 49, 106, 231, 188, 22, 247, 221, 163, 252, 20, 146, 229, 246, 134, 11, 85, 67, 57, 90, 81, 233, 82, 0, 0, 0, 1, 102, 234, 243, 251, 0, 0, 0, 0, 0} +var authSHA256Packet1 = []byte{56, 241, 168, 141, 190, 188, 201, 75, 111, 199, 1, 198, 27, 138, 167, 106, 34, 70, 142, 66, 147, 64, 216, 37, 38, 62, 8, 150, 42, 120, 226, 65, 81, 10, 81, 27, 180, 47, 147, 125, 81, 0, 0, 0, 1, 102, 234, 244, 77, 0, 0, 0, 0, 0} +var authSHA256Packet2 = []byte{56, 198, 120, 101, 184, 178, 101, 227, 112, 52, 242, 119, 31, 128, 235, 50, 107, 58, 233, 34, 122, 77, 17, 220, 196, 226, 154, 108, 211, 182, 10, 155, 196, 199, 66, 72, 174, 72, 44, 220, 36, 0, 0, 0, 1, 102, 235, 45, 106, 0, 0, 0, 0, 0} +var authSHA384Packet1 = []byte{56, 160, 37, 56, 112, 116, 89, 66, 24, 154, 38, 199, 92, 228, 209, 62, 141, 171, 224, 61, 218, 223, 221, 98, 33, 77, 134, 136, 40, 146, 36, 112, 30, 207, 152, 170, 2, 216, 227, 212, 205, 1, 115, 113, 22, 3, 11, 8, 208, 81, 97, 20, 191, 64, 202, 169, 249, 0, 0, 0, 1, 102, 234, 244, 185, 0, 0, 0, 0, 0} +var authSHA384Packet2 = []byte{56, 175, 250, 66, 242, 105, 89, 222, 222, 108, 120, 226, 236, 11, 225, 251, 172, 175, 118, 219, 249, 225, 140, 228, 111, 129, 234, 103, 248, 34, 220, 49, 65, 99, 241, 43, 235, 15, 49, 216, 249, 41, 140, 75, 231, 56, 33, 200, 228, 255, 207, 231, 234, 189, 248, 105, 1, 0, 0, 0, 1, 102, 235, 46, 153, 0, 0, 0, 0, 0} +var authSHA512Packet1 = []byte{56, 161, 94, 244, 194, 238, 125, 66, 225, 158, 56, 169, 182, 153, 161, 60, 52, 18, 97, 185, 50, 29, 118, 249, 132, 174, 102, 134, 41, 219, 138, 47, 121, 94, 151, 157, 117, 100, 50, 28, 187, 17, 127, 71, 193, 79, 142, 107, 174, 210, 123, 68, 207, 70, 40, 98, 73, 118, 125, 217, 193, 236, 245, 181, 36, 237, 68, 214, 150, 103, 239, 47, 69, 0, 0, 0, 1, 102, 234, 245, 25, 0, 0, 0, 0, 0} +var authSHA512Packet2 = []byte{56, 216, 77, 99, 2, 63, 78, 109, 231, 0, 10, 151, 81, 69, 223, 219, 180, 247, 218, 140, 170, 125, 79, 34, 161, 70, 29, 172, 91, 88, 45, 168, 55, 171, 209, 42, 255, 50, 38, 254, 254, 69, 190, 54, 201, 7, 176, 188, 231, 178, 32, 104, 23, 230, 139, 31, 109, 7, 74, 23, 204, 111, 15, 47, 184, 142, 42, 87, 177, 229, 241, 249, 5, 0, 0, 0, 1, 102, 234, 255, 102, 0, 0, 0, 0, 0} +var authSHA512224Packet1 = []byte{56, 120, 93, 159, 15, 39, 241, 197, 215, 124, 49, 249, 190, 40, 30, 103, 24, 237, 160, 8, 161, 166, 93, 197, 148, 86, 250, 10, 149, 235, 99, 28, 241, 101, 144, 232, 87, 0, 0, 0, 1, 102, 235, 51, 45, 0, 0, 0, 0, 0} +var authSHA512224Packet2 = []byte{56, 53, 50, 102, 177, 253, 154, 44, 246, 173, 13, 203, 52, 177, 212, 190, 163, 163, 56, 75, 12, 35, 102, 36, 104, 173, 105, 79, 88, 155, 95, 205, 120, 223, 140, 149, 46, 0, 0, 0, 1, 102, 235, 51, 79, 0, 0, 0, 0, 0} +var authSHA512256Packet1 = []byte{56, 156, 159, 218, 246, 224, 82, 248, 79, 214, 160, 218, 53, 181, 49, 88, 113, 34, 84, 237, 38, 173, 51, 70, 73, 213, 141, 198, 137, 96, 146, 164, 20, 250, 51, 190, 127, 193, 138, 146, 150, 0, 0, 0, 1, 102, 235, 51, 120, 0, 0, 0, 0, 0} +var authSHA512256Packet2 = []byte{56, 186, 196, 224, 32, 227, 61, 79, 121, 104, 237, 52, 167, 134, 171, 65, 80, 24, 151, 202, 16, 228, 171, 154, 174, 102, 41, 163, 190, 181, 203, 116, 114, 212, 38, 182, 6, 85, 139, 204, 151, 0, 0, 0, 1, 102, 235, 51, 146, 0, 0, 0, 0, 0} + +var authSHA3224Packet1 = []byte{56, 75, 11, 103, 27, 91, 109, 41, 244, 55, 38, 214, 34, 145, 221, 10, 39, 122, 95, 31, 247, 145, 61, 200, 0, 50, 20, 138, 13, 157, 64, 45, 229, 228, 103, 188, 122, 0, 0, 0, 1, 102, 235, 49, 75, 0, 0, 0, 0, 0} +var authSHA3224Packet2 = []byte{56, 27, 135, 234, 94, 136, 16, 205, 183, 224, 158, 35, 33, 167, 179, 186, 129, 221, 189, 91, 145, 254, 97, 214, 73, 168, 98, 178, 238, 57, 164, 23, 10, 231, 232, 228, 130, 0, 0, 0, 1, 102, 235, 49, 166, 0, 0, 0, 0, 0} +var authSHA3256Packet1 = []byte{56, 102, 44, 44, 250, 78, 239, 197, 24, 141, 207, 4, 172, 243, 182, 248, 89, 85, 126, 211, 221, 77, 58, 132, 232, 210, 92, 100, 224, 138, 249, 189, 233, 173, 65, 107, 247, 44, 12, 44, 25, 0, 0, 0, 1, 102, 235, 49, 221, 0, 0, 0, 0, 0} +var authSHA3256Packet2 = []byte{56, 111, 169, 160, 21, 96, 100, 39, 245, 239, 232, 248, 180, 118, 223, 2, 151, 181, 0, 11, 135, 228, 62, 200, 44, 74, 41, 61, 165, 219, 6, 140, 9, 232, 100, 126, 61, 31, 78, 112, 114, 0, 0, 0, 1, 102, 235, 49, 255, 0, 0, 0, 0, 0} +var authSHA3384Packet1 = []byte{56, 218, 149, 197, 79, 218, 74, 75, 4, 109, 230, 99, 239, 20, 110, 58, 247, 115, 155, 16, 0, 63, 246, 58, 163, 117, 183, 254, 124, 158, 57, 90, 135, 115, 197, 127, 124, 240, 153, 252, 185, 94, 204, 83, 67, 204, 234, 217, 139, 253, 229, 231, 48, 124, 223, 87, 116, 0, 0, 0, 1, 102, 235, 50, 44, 0, 0, 0, 0, 0} +var authSHA3384Packet2 = []byte{56, 182, 206, 108, 83, 191, 147, 63, 158, 59, 66, 8, 109, 134, 242, 142, 55, 34, 184, 130, 40, 104, 48, 136, 113, 123, 93, 177, 36, 111, 185, 151, 14, 35, 42, 200, 95, 241, 192, 218, 171, 88, 217, 108, 229, 133, 112, 162, 157, 218, 83, 149, 212, 102, 179, 173, 248, 0, 0, 0, 1, 102, 235, 50, 76, 0, 0, 0, 0, 0} +var authSHA3512Packet1 = []byte{56, 56, 51, 121, 50, 97, 82, 27, 97, 63, 0, 247, 55, 225, 68, 192, 42, 21, 209, 75, 160, 240, 219, 32, 232, 98, 25, 109, 168, 157, 235, 66, 58, 236, 39, 211, 113, 42, 63, 83, 156, 141, 128, 86, 58, 252, 72, 252, 160, 4, 49, 100, 226, 2, 23, 206, 245, 74, 243, 19, 37, 96, 95, 45, 66, 114, 214, 204, 242, 76, 169, 149, 188, 0, 0, 0, 1, 102, 235, 50, 116, 0, 0, 0, 0, 0} +var authSHA3512Packet2 = []byte{56, 35, 221, 191, 87, 222, 230, 175, 109, 149, 129, 74, 255, 48, 70, 110, 253, 142, 246, 140, 49, 20, 10, 69, 34, 12, 194, 51, 250, 81, 94, 127, 4, 33, 56, 80, 212, 219, 18, 173, 203, 113, 67, 16, 10, 252, 253, 100, 209, 106, 164, 171, 174, 200, 225, 141, 218, 251, 169, 147, 139, 91, 67, 114, 19, 67, 180, 240, 81, 96, 189, 75, 126, 0, 0, 0, 1, 102, 235, 50, 144, 0, 0, 0, 0, 0} + +var authBLAKE2s256Packet1 = []byte{56, 117, 185, 201, 73, 60, 231, 94, 83, 79, 58, 65, 198, 193, 150, 251, 40, 240, 186, 67, 214, 103, 173, 128, 71, 85, 169, 180, 57, 185, 190, 142, 169, 29, 70, 15, 227, 16, 233, 122, 248, 0, 0, 0, 1, 102, 235, 52, 237, 0, 0, 0, 0, 0} +var authBLAKE2s256Packet2 = []byte{56, 113, 195, 33, 169, 198, 218, 173, 209, 221, 244, 170, 234, 51, 121, 193, 200, 71, 196, 195, 124, 161, 83, 34, 216, 32, 220, 169, 217, 119, 173, 198, 111, 212, 180, 207, 239, 133, 126, 23, 73, 0, 0, 0, 1, 102, 235, 53, 26, 0, 0, 0, 0, 0} +var authBLAKE2b512Packet1 = []byte{56, 85, 88, 174, 47, 95, 41, 94, 246, 10, 210, 140, 132, 252, 217, 139, 220, 49, 214, 53, 127, 38, 150, 43, 148, 226, 184, 99, 168, 70, 117, 193, 144, 71, 193, 51, 66, 175, 1, 199, 90, 168, 165, 252, 200, 183, 163, 149, 209, 150, 81, 156, 180, 62, 40, 71, 169, 157, 115, 55, 227, 142, 235, 232, 186, 16, 163, 153, 165, 225, 206, 65, 218, 0, 0, 0, 1, 102, 235, 53, 76, 0, 0, 0, 0, 0} +var authBLAKE2b512Packet2 = []byte{56, 81, 167, 144, 212, 181, 169, 247, 183, 247, 121, 237, 117, 202, 78, 166, 17, 78, 117, 65, 216, 21, 66, 25, 153, 188, 93, 74, 87, 190, 188, 117, 121, 177, 19, 55, 28, 183, 97, 209, 51, 33, 207, 175, 170, 42, 132, 26, 136, 166, 120, 154, 94, 36, 33, 5, 43, 244, 234, 80, 16, 72, 109, 54, 200, 77, 191, 229, 251, 101, 62, 90, 10, 0, 0, 0, 1, 102, 235, 53, 114, 0, 0, 0, 0, 0} + +var authMD5SHA1Packet1 = []byte{56, 179, 179, 82, 117, 88, 75, 7, 103, 207, 125, 244, 183, 3, 111, 46, 96, 79, 33, 216, 220, 7, 46, 234, 213, 182, 38, 87, 48, 131, 127, 227, 208, 13, 246, 26, 169, 220, 143, 161, 18, 68, 67, 179, 92, 0, 0, 0, 1, 102, 235, 48, 73, 0, 0, 0, 0, 0} +var authMD5SHA1Packet2 = []byte{56, 72, 91, 69, 91, 123, 156, 4, 2, 200, 34, 158, 108, 40, 218, 3, 95, 19, 203, 170, 36, 86, 29, 207, 40, 251, 124, 79, 93, 174, 221, 45, 22, 18, 125, 250, 150, 82, 37, 64, 100, 108, 251, 29, 114, 0, 0, 0, 1, 102, 235, 48, 132, 0, 0, 0, 0, 0} + +var authSM3Packet1 = []byte{56, 61, 163, 194, 51, 225, 14, 218, 181, 76, 125, 35, 206, 68, 24, 66, 176, 84, 237, 88, 38, 121, 213, 67, 33, 172, 83, 167, 103, 89, 82, 122, 7, 166, 156, 79, 26, 67, 191, 210, 226, 0, 0, 0, 1, 102, 235, 50, 211, 0, 0, 0, 0, 0} +var authSM3Packet2 = []byte{56, 216, 20, 196, 148, 131, 37, 73, 181, 55, 166, 108, 86, 143, 5, 25, 20, 21, 149, 77, 221, 237, 110, 232, 237, 23, 40, 231, 192, 225, 197, 160, 172, 49, 130, 178, 78, 12, 143, 130, 229, 0, 0, 0, 1, 102, 235, 50, 243, 0, 0, 0, 0, 0} + +var authWhirlpoolPacket1 = []byte{56, 36, 245, 218, 233, 15, 158, 142, 144, 116, 207, 71, 60, 35, 210, 55, 90, 223, 92, 122, 87, 53, 131, 122, 248, 84, 50, 42, 69, 254, 231, 197, 30, 216, 65, 242, 173, 160, 127, 229, 165, 224, 32, 16, 118, 45, 197, 158, 145, 0, 130, 72, 78, 104, 107, 247, 100, 54, 185, 151, 70, 116, 219, 15, 112, 175, 78, 30, 177, 222, 41, 223, 29, 0, 0, 0, 1, 102, 235, 55, 2, 0, 0, 0, 0, 0} +var authWhirlpoolPacket2 = []byte{56, 31, 112, 117, 225, 131, 182, 114, 117, 242, 139, 201, 7, 108, 19, 165, 164, 120, 243, 26, 152, 195, 228, 36, 162, 249, 105, 116, 114, 121, 232, 215, 253, 153, 249, 243, 54, 55, 83, 70, 28, 19, 87, 233, 54, 164, 69, 52, 96, 28, 107, 220, 236, 226, 35, 224, 155, 100, 78, 59, 174, 220, 107, 120, 56, 100, 160, 122, 182, 96, 94, 83, 71, 0, 0, 0, 1, 102, 235, 55, 144, 0, 0, 0, 0, 0} + +var cryptPacket1 = []byte{56, 76, 98, 159, 244, 184, 134, 148, 158, 0, 0, 0, 1, 102, 237, 91, 50, 14, 141, 87, 40, 125, 165, 204, 227, 61, 5, 91, 201, 99, 44, 253, 7, 202, 200, 84, 124, 48, 80, 144, 250, 52, 248, 173, 26, 201, 173, 67, 166, 16, 189, 73, 203, 12} +var cryptPacket2 = []byte{56, 162, 49, 153, 71, 88, 124, 182, 93, 0, 0, 0, 1, 102, 237, 91, 95, 84, 154, 63, 127, 63, 175, 65, 227, 69, 45, 146, 14, 64, 81, 56, 239, 162, 229, 54, 81, 103, 167, 133, 38, 57, 83, 119, 60, 149, 149, 218, 201, 144, 193, 202, 149, 111} + +/* + * All the sample packets below are generated with another static key. + */ + +var cryptPacket3 = []byte{56, 114, 151, 86, 204, 204, 137, 212, 215, 0, 0, 0, 1, 102, 231, 24, 196, 58, 184, 197, 69, 200, 222, 132, 120, 248, 163, 68, 112, 17, 137, 97, 240, 56, 122, 62, 49, 172, 177, 176, 86, 180, 187, 148, 69, 17, 251, 38, 0, 31, 203, 0, 237, 122} +var cryptPacket4 = []byte{56, 49, 193, 232, 78, 82, 175, 151, 76, 0, 0, 0, 1, 102, 231, 25, 92, 82, 125, 47, 131, 35, 217, 41, 164, 145, 71, 178, 38, 218, 194, 60, 100, 167, 212, 8, 160, 131, 22, 61, 246, 52, 20, 100, 6, 16, 108, 18, 127, 24, 185, 240, 99, 156} + +var authSHA512Packet3 = []byte{56, 9, 137, 51, 217, 234, 95, 85, 78, 254, 110, 108, 95, 38, 212, 11, 224, 47, 57, 16, 51, 199, 136, 76, 111, 191, 16, 107, 75, 219, 113, 162, 191, 67, 46, 146, 184, 246, 177, 52, 53, 53, 127, 191, 5, 184, 24, 166, 146, 223, 234, 222, 239, 9, 92, 227, 241, 225, 196, 46, 230, 138, 3, 5, 85, 186, 65, 251, 189, 11, 16, 28, 102, 0, 0, 0, 1, 102, 231, 16, 138, 0, 0, 0, 0, 0} +var authSHA512Packet4 = []byte{56, 178, 216, 6, 201, 115, 66, 0, 252, 112, 99, 34, 163, 140, 85, 246, 137, 75, 183, 212, 159, 38, 251, 25, 190, 253, 36, 249, 198, 196, 70, 177, 201, 14, 65, 227, 248, 77, 108, 115, 189, 160, 244, 174, 98, 107, 141, 70, 231, 120, 91, 118, 74, 229, 197, 11, 34, 193, 58, 35, 253, 148, 135, 235, 90, 101, 6, 152, 24, 139, 17, 204, 33, 0, 0, 0, 1, 102, 231, 16, 206, 0, 0, 0, 0, 0} +var authSHA384Packet3 = []byte{56, 219, 201, 226, 49, 70, 125, 55, 178, 191, 78, 40, 216, 206, 58, 20, 224, 132, 135, 191, 172, 205, 188, 24, 176, 48, 143, 139, 127, 225, 202, 39, 8, 196, 77, 57, 9, 41, 94, 103, 73, 169, 38, 206, 220, 2, 48, 62, 228, 47, 75, 97, 94, 55, 92, 204, 186, 0, 0, 0, 1, 102, 231, 17, 154, 0, 0, 0, 0, 0} +var authSHA384Packet4 = []byte{56, 16, 226, 49, 167, 146, 222, 219, 13, 3, 106, 151, 111, 210, 227, 142, 102, 16, 216, 234, 94, 111, 244, 11, 94, 253, 12, 186, 117, 92, 196, 92, 65, 107, 141, 17, 229, 249, 197, 17, 103, 41, 223, 153, 181, 117, 29, 117, 56, 22, 175, 162, 64, 31, 77, 122, 72, 0, 0, 0, 1, 102, 231, 17, 193, 0, 0, 0, 0, 0} +var authMD5SHA1Packet3 = []byte{56, 99, 167, 201, 107, 123, 246, 212, 180, 87, 93, 31, 188, 10, 53, 149, 139, 232, 13, 207, 71, 108, 154, 143, 114, 180, 196, 221, 157, 16, 106, 225, 14, 219, 137, 223, 222, 146, 106, 226, 168, 120, 86, 22, 124, 0, 0, 0, 1, 102, 231, 23, 250, 0, 0, 0, 0, 0} +var authMD5SHA1Packet4 = []byte{56, 37, 80, 49, 19, 199, 20, 62, 202, 74, 99, 211, 73, 42, 204, 120, 193, 83, 126, 182, 7, 159, 177, 126, 206, 70, 29, 198, 68, 211, 249, 15, 123, 201, 45, 193, 38, 134, 223, 186, 236, 58, 235, 55, 130, 0, 0, 0, 1, 102, 231, 24, 40, 0, 0, 0, 0, 0} +var authSHA256Packet3 = []byte{56, 102, 151, 183, 239, 253, 36, 110, 23, 150, 73, 73, 166, 35, 204, 199, 240, 149, 243, 16, 8, 55, 68, 108, 31, 11, 74, 186, 254, 65, 15, 81, 5, 222, 184, 12, 106, 72, 2, 114, 154, 0, 0, 0, 1, 102, 231, 17, 9, 0, 0, 0, 0, 0} +var authSHA256Packet4 = []byte{56, 95, 17, 119, 145, 64, 76, 195, 82, 28, 120, 32, 8, 114, 93, 80, 206, 88, 123, 172, 37, 73, 97, 54, 221, 37, 7, 157, 39, 147, 73, 251, 107, 61, 52, 76, 11, 97, 161, 3, 96, 0, 0, 0, 1, 102, 231, 17, 93, 0, 0, 0, 0, 0} +var authSHA224Packet3 = []byte{56, 27, 28, 60, 231, 13, 31, 116, 190, 88, 126, 12, 34, 137, 96, 59, 7, 91, 163, 246, 60, 2, 38, 129, 69, 217, 9, 24, 18, 36, 210, 88, 86, 2, 226, 0, 96, 0, 0, 0, 1, 102, 231, 20, 101, 0, 0, 0, 0, 0} +var authSHA224Packet4 = []byte{56, 248, 223, 183, 225, 174, 116, 5, 214, 134, 211, 177, 21, 142, 215, 9, 8, 164, 55, 40, 10, 206, 40, 254, 173, 235, 176, 126, 12, 67, 35, 221, 219, 209, 30, 244, 178, 0, 0, 0, 1, 102, 231, 20, 137, 0, 0, 0, 0, 0} +var authSHA1Packet3 = []byte{56, 237, 200, 39, 23, 233, 70, 6, 161, 241, 68, 106, 124, 33, 176, 55, 84, 222, 250, 76, 156, 191, 179, 213, 51, 159, 4, 62, 210, 0, 0, 0, 1, 102, 231, 18, 45, 0, 0, 0, 0, 0} +var authSHA1Packet4 = []byte{56, 130, 31, 160, 196, 239, 31, 197, 116, 52, 169, 99, 61, 200, 67, 148, 130, 219, 9, 88, 119, 50, 245, 69, 146, 204, 29, 211, 206, 0, 0, 0, 1, 102, 231, 18, 99, 0, 0, 0, 0, 0} +var authMD5Packet3 = []byte{56, 215, 5, 24, 98, 120, 183, 161, 99, 207, 88, 65, 149, 207, 91, 106, 49, 202, 38, 190, 180, 159, 186, 132, 12, 0, 0, 0, 1, 102, 231, 18, 140, 0, 0, 0, 0, 0} +var authMD5Packet4 = []byte{56, 54, 43, 62, 107, 142, 244, 8, 206, 17, 95, 99, 7, 97, 10, 102, 63, 210, 191, 101, 209, 192, 183, 94, 67, 0, 0, 0, 1, 102, 231, 18, 188, 0, 0, 0, 0, 0} + +var plainPacket3 = []uint8{56, 232, 90, 55, 186, 10, 31, 142, 127, 0, 0, 0, 0, 0} +var plainPacket4 = []uint8{56, 177, 22, 70, 225, 86, 175, 190, 204, 0, 0, 0, 0, 0} + +/* + * All the sample packets below are generated with a pair of tls-crypt-v2 server key and tls-crypt-v2 client key. + */ + +var serverKey56Base64 = "U2hihe8H77pInpRzMEWNZ/NwM1CBSSVSw5HyXT7/+1pspISJzKBiECs+LRvE6QlwgKm606H1wLv0defgJRNU1UG1fi25oMPqjFcYybU+wOgY8eX6OWM0EWI6d2XaL6Neu1E9fMGDAWnzQFsFZhMQH80xv0kzzLm13UjL7lrdQnM=" +var clientKey56Base64 = "HZVyTZ3S2YMR9UFUei+kWmNcCaxxT31StqhozQFXVQ41WK203PFtunbuA7HZNPaBLQbyC3aaxwGcEsqW1Jnm/3WptcPWFYhFGhW+H37x2howQyAGj6IIsjZQyS9gwYgGr8bVNTZIywz3hw+KLRCAzkhTqk6ONen1wf5rewu03g2RNq/suLU6V31OTDOxeyb1WkUA25Ych7le6FJzO8YqI5jOosokID3ueT05vCdMIDa6FsHR3BmPjX2OYLquV+wBF7IBykKcxrCvrT2Qf/tBpv7PtUkKx6pCApKiUuPxLALMxqv3ATa7vrCB8qZQWmO4dRY40ORYZ622MiqCDmDFGKQca1bPxdHdpFCr5o8gvYHR9p0Xm2t6KcCvAO+CUAMKA1DsUqIYrrhQIkkZI/dbffBGnxyrb/XlzGnSynv5d8oAl7QSrW292JLtm7VCGE7NePEMvAx9iAXNTwxpdwNNfPmb3ZNWq7H+mGUaYQpcs7xW45zzyUkV9fESFEnlePxeJ3pjqxawcjnOBy1yLk/RKyFOakNJ/Z6r7IFjgI6usooo1i7+Tp6O5MHQasWcpyLWzW5KvjGPIKH0SFl6VJdBMkWWYm1D5KdgqXX8716uNvpY0dVpXgE+KfJPxIRQDGCuS8zyrxAHCvm8MAbiOpi9g35/4vKFj+cvar4EssHrvSX+l0Z7q5kGnMbnvStH7MEzTQXCcMT30vDWBn2W9SEkOh9KIq1z3/46owEr" + +var serverKey56 = StaticKeyNewFromBase64(serverKey56Base64, false, false) +var clientKey56 = WrappedKeyNewFromBase64(clientKey56Base64) + +var crypt2Packet5 = []byte{80, 100, 224, 45, 159, 27, 166, 162, 220, 15, 0, 0, 1, 102, 240, 100, 162, 53, 85, 10, 213, 183, 32, 34, 176, 186, 16, 66, 59, 48, 128, 24, 240, 143, 116, 59, 133, 18, 152, 241, 84, 81, 95, 195, 181, 88, 112, 148, 217, 127, 200, 222, 197, 88, 164, 28, 107, 86, 207, 197, 209, 221, 164, 80, 171, 230, 143, 32, 189, 129, 209, 246, 157, 23, 155, 107, 122, 41, 192, 175, 0, 239, 130, 80, 3, 10, 3, 80, 236, 82, 162, 24, 174, 184, 80, 34, 73, 25, 35, 247, 91, 125, 240, 70, 159, 28, 171, 111, 245, 229, 204, 105, 210, 202, 123, 249, 119, 202, 0, 151, 180, 18, 173, 109, 189, 216, 146, 237, 155, 181, 66, 24, 78, 205, 120, 241, 12, 188, 12, 125, 136, 5, 205, 79, 12, 105, 119, 3, 77, 124, 249, 155, 221, 147, 86, 171, 177, 254, 152, 101, 26, 97, 10, 92, 179, 188, 86, 227, 156, 243, 201, 73, 21, 245, 241, 18, 20, 73, 229, 120, 252, 94, 39, 122, 99, 171, 22, 176, 114, 57, 206, 7, 45, 114, 46, 79, 209, 43, 33, 78, 106, 67, 73, 253, 158, 171, 236, 129, 99, 128, 142, 174, 178, 138, 40, 214, 46, 254, 78, 158, 142, 228, 193, 208, 106, 197, 156, 167, 34, 214, 205, 110, 74, 190, 49, 143, 32, 161, 244, 72, 89, 122, 84, 151, 65, 50, 69, 150, 98, 109, 67, 228, 167, 96, 169, 117, 252, 239, 94, 174, 54, 250, 88, 209, 213, 105, 94, 1, 62, 41, 242, 79, 196, 132, 80, 12, 96, 174, 75, 204, 242, 175, 16, 7, 10, 249, 188, 48, 6, 226, 58, 152, 189, 131, 126, 127, 226, 242, 133, 143, 231, 47, 106, 190, 4, 178, 193, 235, 189, 37, 254, 151, 70, 123, 171, 153, 6, 156, 198, 231, 189, 43, 71, 236, 193, 51, 77, 5, 194, 112, 196, 247, 210, 240, 214, 6, 125, 150, 245, 33, 36, 58, 31, 74, 34, 173, 115, 223, 254, 58, 163, 1, 43} +var crypt2Packet6 = []byte{80, 136, 240, 154, 124, 25, 199, 138, 59, 15, 0, 0, 1, 102, 240, 107, 4, 103, 91, 3, 26, 182, 97, 79, 186, 12, 192, 49, 251, 104, 205, 177, 215, 107, 141, 155, 102, 232, 247, 246, 206, 142, 216, 230, 20, 218, 58, 153, 248, 131, 173, 105, 2, 213, 164, 28, 107, 86, 207, 197, 209, 221, 164, 80, 171, 230, 143, 32, 189, 129, 209, 246, 157, 23, 155, 107, 122, 41, 192, 175, 0, 239, 130, 80, 3, 10, 3, 80, 236, 82, 162, 24, 174, 184, 80, 34, 73, 25, 35, 247, 91, 125, 240, 70, 159, 28, 171, 111, 245, 229, 204, 105, 210, 202, 123, 249, 119, 202, 0, 151, 180, 18, 173, 109, 189, 216, 146, 237, 155, 181, 66, 24, 78, 205, 120, 241, 12, 188, 12, 125, 136, 5, 205, 79, 12, 105, 119, 3, 77, 124, 249, 155, 221, 147, 86, 171, 177, 254, 152, 101, 26, 97, 10, 92, 179, 188, 86, 227, 156, 243, 201, 73, 21, 245, 241, 18, 20, 73, 229, 120, 252, 94, 39, 122, 99, 171, 22, 176, 114, 57, 206, 7, 45, 114, 46, 79, 209, 43, 33, 78, 106, 67, 73, 253, 158, 171, 236, 129, 99, 128, 142, 174, 178, 138, 40, 214, 46, 254, 78, 158, 142, 228, 193, 208, 106, 197, 156, 167, 34, 214, 205, 110, 74, 190, 49, 143, 32, 161, 244, 72, 89, 122, 84, 151, 65, 50, 69, 150, 98, 109, 67, 228, 167, 96, 169, 117, 252, 239, 94, 174, 54, 250, 88, 209, 213, 105, 94, 1, 62, 41, 242, 79, 196, 132, 80, 12, 96, 174, 75, 204, 242, 175, 16, 7, 10, 249, 188, 48, 6, 226, 58, 152, 189, 131, 126, 127, 226, 242, 133, 143, 231, 47, 106, 190, 4, 178, 193, 235, 189, 37, 254, 151, 70, 123, 171, 153, 6, 156, 198, 231, 189, 43, 71, 236, 193, 51, 77, 5, 194, 112, 196, 247, 210, 240, 214, 6, 125, 150, 245, 33, 36, 58, 31, 74, 34, 173, 115, 223, 254, 58, 163, 1, 43} diff --git a/modules/l4openvpn/messages.go b/modules/l4openvpn/messages.go new file mode 100644 index 0000000..ed101eb --- /dev/null +++ b/modules/l4openvpn/messages.go @@ -0,0 +1,948 @@ +// Copyright 2024 VNXME +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package l4openvpn + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "errors" + "slices" + "time" +) + +// MessageHeader is an OpenVPN message header. +type MessageHeader struct { + // Opcode is a type of message that follows. + Opcode uint8 + // KeyID refers to an already negotiated TLS session. It must equal zero for client reset messages. + KeyID uint8 +} + +// FromBytes fills msg's internal structures from source bytes. +func (msg *MessageHeader) FromBytes(src []byte) error { + // Any MessageHeader has exactly 1 byte. + if len(src) != OpcodeKeyIDBytesTotal { + return ErrInvalidSourceLength + } + + // Opcode occupies the high 5 bits, KeyID extends over the low 3 bits. + msg.KeyID, msg.Opcode = src[0]&KeyIDMask, src[0]>>OpcodeShift + + return nil +} + +// ToBytes return a slice of bytes representing msg's internal structures. +func (msg *MessageHeader) ToBytes() []byte { + dst := make([]byte, 0, OpcodeKeyIDBytesTotal) + dst = append(dst, msg.KeyID|(msg.Opcode< 0 && msg.PrevPacketIDsCount == 0 && msg.ThisPacketID == 0 +} + +// ToBytes return a slice of bytes representing msg's internal structures. +func (msg *MessagePlain) ToBytes() []byte { + dst := make([]byte, 0, MessagePlainBytesTotal) + dst = append(dst, msg.MessageHeader.ToBytes()...) + dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID) + dst = append(dst, msg.PrevPacketIDsCount) + dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID) + return dst +} + +// MessageAuth is a P_CONTROL_HARD_RESET_CLIENT_V2-type OpenVPN message with authentication. +type MessageAuth struct { + MessagePlain + MessageTraitAuth + MessageTraitReplay +} + +// Authenticate returns true if msg has a valid HMAC. +func (msg *MessageAuth) Authenticate(ad *AuthDigest, sk *StaticKey) bool { + return msg.MessageTraitAuth.AuthenticateOnServer(ad, sk, msg.ToBytesAuth) +} + +// FromBytes fills msg's internal structures from a slice of bytes. +func (msg *MessageAuth) FromBytes(src []byte) error { + // Any MessageAuth is between 38 and 86 bytes (with a header). + if len(src) < MessageAuthBytesMin || len(src) > MessageAuthBytesMax { + return ErrInvalidSourceLength + } + + // Parse MessageHeader + hdr := &MessageHeader{} + if err := hdr.FromBytes(src[:OpcodeKeyIDBytesTotal]); err != nil { + return err + } + + // Match MessageHeader.Opcode + if hdr.Opcode != OpcodeControlHardResetClientV2 { + return ErrInvalidHeaderOpcode + } + + // Parse everything else + return msg.FromBytesHeadless(src[OpcodeKeyIDBytesTotal:], hdr) +} + +// FromBytesHeadless fills msg's internal structures from a slice of bytes and uses a pre-filled header. +func (msg *MessageAuth) FromBytesHeadless(src []byte, hdr *MessageHeader) error { + // Any MessageAuth is between 37 and 85 bytes long (without a header). + if len(src) < MessageAuthBytesMinHL || len(src) > MessageAuthBytesMaxHL { + return ErrInvalidSourceLength + } + + // Check for a non-empty MessageHeader. + if hdr == nil { + return ErrMissingReusableHeader + } + + // Assign Header to the specified pointer. + msg.MessageHeader = *hdr + + // Any MessageAuth has an 8-byte LocalSessionID at the beginning. + msg.LocalSessionID = BytesOrder.Uint64(src[:SessionIDBytesTotal]) + + // Any MessageAuth has a 16-to-64-byte HMAC between LocalSessionID and ReplayPacketID. + off1, off2 := SessionIDBytesTotal, len(src)-2*PacketIDBytesTotal-OpcodeKeyIDBytesTotal-TimestampBytesTotal + msg.HMAC = src[off1:off2] + + // Check whether a valid HMAC length is provided. + if !slices.Contains(AuthDigestSizes, len(msg.HMAC)) { + return ErrInvalidHMACLength + } + + // Any MessageAuth has a 4-byte ReplayPacketID between HMAC and ReplayTimestamp. + off1, off2 = off2, off2+PacketIDBytesTotal + msg.ReplayPacketID = BytesOrder.Uint32(src[off1:off2]) + + // Any MessageAuth has a 4-byte ReplayTimestamp between ReplayPacketID and PrevPacketIDsCount. + off1, off2 = off2, off2+TimestampBytesTotal + msg.ReplayTimestamp = BytesOrder.Uint32(src[off1:off2]) + + // Any MessageAuth has a 1-byte PrevPacketIDsCount between ReplayTimestamp and ThisPacketID. + off1, off2 = off2, off2+OpcodeKeyIDBytesTotal + msg.PrevPacketIDsCount = src[off1] + + // Any MessageAuth has a 4-byte ThisPacketID at the end. + msg.ThisPacketID = BytesOrder.Uint32(src[off2:]) + + return nil +} + +// Match returns true if msg's internal structures have valid values. +func (msg *MessageAuth) Match(ignoreTimestamp, ignoreCrypto bool, ad *AuthDigest, sk *StaticKey) bool { + return msg.MessagePlain.Match() && + msg.ReplayPacketID == 1 && (ignoreTimestamp || msg.ValidateReplayTimestamp(time.Now())) && + (ad == nil || ad.Size == len(msg.MessageTraitAuth.HMAC)) && + (ignoreCrypto || sk == nil || msg.Authenticate(ad, sk)) +} + +// Sign computes and fills msg's HMAC. +func (msg *MessageAuth) Sign(ad *AuthDigest, sk *StaticKey) error { + return msg.MessageTraitAuth.SignOnClient(ad, sk, msg.ToBytesAuth) +} + +// ToBytes returns a slice of bytes representing msg's internal structures. +func (msg *MessageAuth) ToBytes() []byte { + dst := make([]byte, 0, MessagePlainBytesTotal+len(msg.HMAC)+PacketIDBytesTotal+TimestampBytesTotal) + dst = append(dst, msg.MessageHeader.ToBytes()...) + dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID) + dst = append(dst, msg.HMAC...) + dst = BytesOrder.AppendUint32(dst, msg.ReplayPacketID) + dst = BytesOrder.AppendUint32(dst, msg.ReplayTimestamp) + dst = append(dst, msg.PrevPacketIDsCount) + dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID) + return dst +} + +// ToBytesAuth returns a slice of bytes representing msg's internal structures without HMAC. +func (msg *MessageAuth) ToBytesAuth() []byte { + dst := make([]byte, 0, MessagePlainBytesTotal+PacketIDBytesTotal+TimestampBytesTotal) + dst = BytesOrder.AppendUint32(dst, msg.ReplayPacketID) + dst = BytesOrder.AppendUint32(dst, msg.ReplayTimestamp) + dst = append(dst, msg.MessageHeader.ToBytes()...) + dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID) + dst = append(dst, msg.PrevPacketIDsCount) + dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID) + return dst +} + +// MessageCrypt is a P_CONTROL_HARD_RESET_CLIENT_V2-type OpenVPN message with authentication and encryption. +type MessageCrypt struct { + MessageAuth + MessageTraitCrypt +} + +// Authenticate returns true if msg has a valid HMAC. +func (msg *MessageCrypt) Authenticate(ad *AuthDigest, sk *StaticKey) bool { + return msg.MessageTraitAuth.AuthenticateOnServer(ad, sk, msg.ToBytesAuth) +} + +// DecryptAndAuthenticate decrypts msg's encrypted bytes before calling Authenticate. +func (msg *MessageCrypt) DecryptAndAuthenticate(ad *AuthDigest, sk *StaticKey) bool { + if len(msg.Encrypted) != OpcodeKeyIDBytesTotal+PacketIDBytesTotal || + msg.MessageTraitCrypt.DecryptOnServer(sk, &msg.MessageTraitAuth, msg.FromBytesCrypt) != nil { + return false + } + return msg.Authenticate(ad, sk) +} + +// EncryptAndSign encrypts msg's plain bytes before calling Sign. +func (msg *MessageCrypt) EncryptAndSign(ad *AuthDigest, sk *StaticKey) error { + if err := msg.MessageTraitCrypt.EncryptOnClient(sk, &msg.MessageTraitAuth, msg.ToBytesCrypt); err != nil { + return err + } + return msg.Sign(ad, sk) +} + +// FromBytes fills msg's internal structures from a slice of bytes. +func (msg *MessageCrypt) FromBytes(src []byte) error { + // Any MessageCrypt is between 54 bytes (with a header). + if len(src) != MessageCryptBytesTotal { + return ErrInvalidSourceLength + } + + // Parse MessageHeader + hdr := &MessageHeader{} + if err := hdr.FromBytes(src[:OpcodeKeyIDBytesTotal]); err != nil { + return err + } + + // Match MessageHeader.Opcode + if hdr.Opcode != OpcodeControlHardResetClientV2 { + return ErrInvalidHeaderOpcode + } + + // Parse everything else + return msg.FromBytesHeadless(src[OpcodeKeyIDBytesTotal:], hdr) +} + +// FromBytesHeadless fills msg's internal structures from a slice of bytes and uses a pre-filled header. +func (msg *MessageCrypt) FromBytesHeadless(src []byte, hdr *MessageHeader) error { + // Any MessageCrypt is exactly 53 bytes long (without a header). + if len(src) != MessageCryptBytesTotalHL { + return ErrInvalidSourceLength + } + + // Check for a non-empty MessageHeader. + if hdr == nil { + return ErrMissingReusableHeader + } + + // Assign Header to the specified pointer. + msg.MessageHeader = *hdr + + // Any MessageCrypt has an 8-byte LocalSessionID at the beginning. + msg.LocalSessionID = BytesOrder.Uint64(src[:SessionIDBytesTotal]) + + // Any MessageCrypt has a 4-byte ReplayPacketID between LocalSessionID and ReplayTimestamp. + off1, off2 := SessionIDBytesTotal, SessionIDBytesTotal+PacketIDBytesTotal + msg.ReplayPacketID = BytesOrder.Uint32(src[off1:off2]) + + // Any MessageCrypt has a 4-byte ReplayTimestamp between ReplayPacketID and HMAC. + off1, off2 = off2, off2+TimestampBytesTotal + msg.ReplayTimestamp = BytesOrder.Uint32(src[off1:off2]) + + // Any MessageCrypt has a 32-byte SHA-256 HMAC between ReplayTimestamp and Encrypted bytes. + off1, off2 = off2, off2+AuthDigestDefault.Size + msg.Digest, msg.HMAC = AuthDigestDefault, src[off1:off2] + + // Any MessageCrypt has a 5-byte Encrypted part at the end. + msg.Cipher, msg.Encrypted = CryptCipherDefault, src[off2:] + + return nil +} + +// FromBytesCrypt fills msg's internal structures from a slice of bytes after decryption. +func (msg *MessageCrypt) FromBytesCrypt(plain []byte) error { + if len(plain) != len(msg.Encrypted) { + return ErrInvalidPlainLength + } + + // Any MessageCrypt has a PrevPacketIDsCount at the beginning of the plain text. + msg.PrevPacketIDsCount = plain[0] + + // Any MessageCrypt has a ThisPacketID at the end of the plain text. + msg.ThisPacketID = BytesOrder.Uint32(plain[OpcodeKeyIDBytesTotal:]) + + return nil +} + +// Match returns true if msg's internal structures have valid values. +func (msg *MessageCrypt) Match(ignoreTimestamp, ignoreCrypto bool, ad *AuthDigest, sk *StaticKey) bool { + return msg.LocalSessionID > 0 && + msg.ReplayPacketID == 1 && (ignoreTimestamp || msg.ValidateReplayTimestamp(time.Now())) && + (ignoreCrypto || sk == nil || (msg.DecryptAndAuthenticate(ad, sk) && + msg.PrevPacketIDsCount == 0 && msg.ThisPacketID == 0)) +} + +// Sign computes and fills msg's HMAC. +func (msg *MessageCrypt) Sign(ad *AuthDigest, sk *StaticKey) error { + return msg.MessageTraitAuth.SignOnClient(ad, sk, msg.ToBytesAuth) +} + +// ToBytes returns a slice of bytes representing msg's internal structures. +func (msg *MessageCrypt) ToBytes() []byte { + dst := make([]byte, 0, MessagePlainBytesTotal+len(msg.HMAC)+PacketIDBytesTotal+TimestampBytesTotal) + dst = append(dst, msg.MessageHeader.ToBytes()...) + dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID) + dst = BytesOrder.AppendUint32(dst, msg.ReplayPacketID) + dst = BytesOrder.AppendUint32(dst, msg.ReplayTimestamp) + dst = append(dst, msg.HMAC...) + dst = append(dst, msg.Encrypted...) + return dst +} + +// ToBytesAuth returns a slice of bytes representing msg's internal structures without HMAC. +func (msg *MessageCrypt) ToBytesAuth() []byte { + dst := make([]byte, 0, MessagePlainBytesTotal+PacketIDBytesTotal+TimestampBytesTotal) + dst = append(dst, msg.MessageHeader.ToBytes()...) + dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID) + dst = BytesOrder.AppendUint32(dst, msg.ReplayPacketID) + dst = BytesOrder.AppendUint32(dst, msg.ReplayTimestamp) + dst = append(dst, msg.PrevPacketIDsCount) + dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID) + return dst +} + +// ToBytesCrypt returns a slice of bytes representing msg's internal structures before encryption. +func (msg *MessageCrypt) ToBytesCrypt() []byte { + dst := make([]byte, 0, OpcodeKeyIDBytesTotal+PacketIDBytesTotal) + dst = append(dst, msg.PrevPacketIDsCount) + dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID) + return dst +} + +// MessageCrypt2 is a P_CONTROL_HARD_RESET_CLIENT_V3-type OpenVPN message with authentication and encryption +// using a wrapped client key which is authenticated and encrypted with a server key. +type MessageCrypt2 struct { + MessageCrypt + WrappedKey +} + +// DecryptAndAuthenticate decrypts and authenticates msg's encrypted bytes (WrappedKey before MessageCrypt). +func (msg *MessageCrypt2) DecryptAndAuthenticate(ad *AuthDigest, sk *StaticKey) bool { + if !msg.WrappedKey.DecryptAndAuthenticate(ad, sk) { + return false + } + return msg.MessageCrypt.DecryptAndAuthenticate(ad, &msg.WrappedKey.StaticKey) +} + +// EncryptAndSign encrypts and signs msg's plain bytes (WrappedKey before MessageCrypt). +func (msg *MessageCrypt2) EncryptAndSign(ad *AuthDigest, sk *StaticKey) error { + if err := msg.WrappedKey.EncryptAndSign(ad, sk); err != nil { + return err + } + return msg.MessageCrypt.EncryptAndSign(ad, &msg.WrappedKey.StaticKey) +} + +// FromBytes fills msg's internal structures from a slice of bytes. +func (msg *MessageCrypt2) FromBytes(src []byte) error { + // Any MessageCrypt2 is between 344 and 600 bytes (with a header). + if len(src) < MessageCrypt2BytesMin || len(src) > MessageCrypt2BytesMax { + return ErrInvalidSourceLength + } + + // Parse MessageHeader + hdr := &MessageHeader{} + if err := hdr.FromBytes(src[:OpcodeKeyIDBytesTotal]); err != nil { + return err + } + + // Match MessageHeader.Opcode + if hdr.Opcode != OpcodeControlHardResetClientV3 { + return ErrInvalidHeaderOpcode + } + + // Parse everything else + return msg.FromBytesHeadless(src[OpcodeKeyIDBytesTotal:], hdr) +} + +// FromBytesHeadless fills msg's internal structures from a slice of bytes and uses a pre-filled header. +func (msg *MessageCrypt2) FromBytesHeadless(src []byte, hdr *MessageHeader) error { + // Any MessageCrypt2 is between 343 and 1077 bytes long (without a header). + if len(src) < MessageCrypt2BytesMinHL || len(src) > MessageCrypt2BytesMaxHL { + return ErrInvalidSourceLength + } + + if err := msg.MessageCrypt.FromBytesHeadless(src[:MessageCryptBytesTotalHL], hdr); err != nil { + return err + } + + return msg.WrappedKey.FromBytes(src[MessageCryptBytesTotalHL:]) +} + +// Match returns true if msg's internal structures have valid values. +func (msg *MessageCrypt2) Match(ignoreTimestamp, ignoreCrypto bool, ad *AuthDigest, sk *StaticKey, cks []*WrappedKey) bool { + if !(msg.LocalSessionID > 0 && (msg.ReplayPacketID == 1 || msg.ReplayPacketID == 0x0f000001) && + (ignoreTimestamp || msg.ValidateReplayTimestamp(time.Now()))) { + return false + } + + if ignoreCrypto { + return true + } + + if len(cks) > 0 { + for _, ck := range cks { + if bytes.Equal(ck.HMAC, msg.WrappedKey.HMAC) && bytes.Equal(ck.Encrypted, msg.WrappedKey.Encrypted) { + return msg.MessageCrypt.DecryptAndAuthenticate(ad, &ck.StaticKey) && + msg.PrevPacketIDsCount == 0 && msg.ThisPacketID == 0 + } + } + return false + } + + return sk == nil || (msg.DecryptAndAuthenticate(ad, sk) && msg.PrevPacketIDsCount == 0 && msg.ThisPacketID == 0) +} + +// ToBytes returns a slice of bytes representing msg's internal structures. +func (msg *MessageCrypt2) ToBytes() []byte { + return slices.Concat(msg.MessageCrypt.ToBytes(), msg.WrappedKey.ToBytes()) +} + +// MessageTraitAuth contains fields and implements features related to authentication. +type MessageTraitAuth struct { + // Digest is a pointer to AuthDigest to be used for authentication. + Digest *AuthDigest + // HMAC is a hash-based messaged authentication code used to verify message integrity. + HMAC []byte +} + +// AuthenticateOnClient returns true if msg composed on the client has a valid HMAC. +func (msg *MessageTraitAuth) AuthenticateOnClient(ad *AuthDigest, sk *StaticKey, xp func() []byte) bool { + // No supported digest returns an HMAC of 0 bytes. + if len(msg.HMAC) == 0 { + return false + } + + // Check that there is a valid StaticKey. + // A half key may be provided as a server key to validate a WrappedKey. + if sk == nil || !(len(sk.KeyBytes) == StaticKeyBytesTotal || len(sk.KeyBytes) == StaticKeyBytesHalf) { + return false + } + + // Obtain a slice of bytes to compute a digest of. + plain := xp() + + // Firstly, try the given digest. + if ad != nil { + if len(msg.HMAC) == ad.Size && ad.HMACValidateOnClient(sk, plain, msg.HMAC) { + msg.Digest = ad + return true + } + return false + } + + // Secondly, try the last successful one saved as Digest. + if msg.Digest != nil && len(msg.HMAC) == msg.Digest.Size && msg.Digest.HMACValidateOnClient(sk, plain, msg.HMAC) { + return true + } + + // Finally, try all other supported digests one by one. + for _, mad := range AuthDigests { + if mad != msg.Digest && len(msg.HMAC) == mad.Size && mad.HMACValidateOnClient(sk, plain, msg.HMAC) { + msg.Digest = mad + return true + } + } + + return false +} + +// AuthenticateOnServer returns true if msg composed on the server has a valid HMAC. +func (msg *MessageTraitAuth) AuthenticateOnServer(ad *AuthDigest, sk *StaticKey, xp func() []byte) bool { + // No supported digest returns an HMAC of 0 bytes. + if len(msg.HMAC) == 0 { + return false + } + + // Check that there is a valid StaticKey. + if sk == nil || len(sk.KeyBytes) != StaticKeyBytesTotal { + return false + } + + // Obtain a slice of bytes to compute a digest of. + plain := xp() + + // Firstly, try the given digest. + if ad != nil { + if len(msg.HMAC) == ad.Size && ad.HMACValidateOnServer(sk, plain, msg.HMAC) { + msg.Digest = ad + return true + } + return false + } + + // Secondly, try the last successful one saved as Digest. + if msg.Digest != nil && len(msg.HMAC) == msg.Digest.Size && msg.Digest.HMACValidateOnServer(sk, plain, msg.HMAC) { + return true + } + + // Finally, try all other supported digests one by one. + for _, mad := range AuthDigests { + if mad != msg.Digest && len(msg.HMAC) == mad.Size && mad.HMACValidateOnServer(sk, plain, msg.HMAC) { + msg.Digest = mad + return true + } + } + + return false +} + +// SignOnClient computes an HMAC for msg composed on the client. +func (msg *MessageTraitAuth) SignOnClient(ad *AuthDigest, sk *StaticKey, xp func() []byte) error { + // Check that there is a valid StaticKey. + // A half key may be provided as a server key to sign a WrappedKey. + if sk == nil || !(len(sk.KeyBytes) == StaticKeyBytesTotal || len(sk.KeyBytes) == StaticKeyBytesHalf) { + return ErrInvalidAuthPrerequisites + } + + // Obtain a slice of bytes to compute a digest of. + plain := xp() + + // Firstly, try the given digest. + if ad != nil { + msg.HMAC = ad.HMACGenerateOnClient(sk, plain) + } + + // Secondly, try the internal Digest. + if msg.Digest != nil { + msg.HMAC = msg.Digest.HMACGenerateOnClient(sk, plain) + } + + // Finally, use the default digest. + msg.Digest, msg.HMAC = AuthDigestDefault, AuthDigestDefault.HMACGenerateOnClient(sk, plain) + + return nil +} + +// SignOnServer computes an HMAC for msg composed on the server. +func (msg *MessageTraitAuth) SignOnServer(ad *AuthDigest, sk *StaticKey, xp func() []byte) error { + // Check that there is a valid StaticKey. + // A half key may be provided as a server key to sign a WrappedKey. + if sk == nil || !(len(sk.KeyBytes) == StaticKeyBytesTotal || len(sk.KeyBytes) == StaticKeyBytesHalf) { + return ErrInvalidAuthPrerequisites + } + + // Obtain a slice of bytes to compute a digest of. + plain := xp() + + // Firstly, try the given digest. + if ad != nil { + msg.HMAC = ad.HMACGenerateOnServer(sk, plain) + } + + // Secondly, try the internal Digest. + if msg.Digest != nil { + msg.HMAC = msg.Digest.HMACGenerateOnServer(sk, plain) + } + + // Finally, use the default digest. + msg.Digest, msg.HMAC = AuthDigestDefault, AuthDigestDefault.HMACGenerateOnServer(sk, plain) + + return nil +} + +// MessageTraitCrypt contains fields and implements features related to en-/decryption. +type MessageTraitCrypt struct { + // Cipher is a pointer to CryptCipher to be used for encryption. + Cipher *CryptCipher + // Encrypted contains a number of bytes to be decrypted. + Encrypted []byte +} + +// DecryptOnClient decrypts msg's encrypted bytes with an encryption key used by the server. +func (msg *MessageTraitCrypt) DecryptOnClient(sk *StaticKey, ta *MessageTraitAuth, xd func([]byte) error) error { + if sk == nil || msg.Cipher == nil || ta.Digest == nil || len(ta.HMAC) != ta.Digest.Size { + return ErrInvalidCryptPrerequisites + } + + plain := msg.Cipher.DecryptOnClient(sk, ta.HMAC[:min(msg.Cipher.SizeBlock, ta.Digest.Size)], msg.Encrypted) + + return xd(plain) +} + +// DecryptOnServer decrypts msg's encrypted bytes with an encryption key used by the client. +func (msg *MessageTraitCrypt) DecryptOnServer(sk *StaticKey, ta *MessageTraitAuth, xd func([]byte) error) error { + if sk == nil || msg.Cipher == nil || ta.Digest == nil || len(ta.HMAC) != ta.Digest.Size { + return ErrInvalidCryptPrerequisites + } + + plain := msg.Cipher.DecryptOnServer(sk, ta.HMAC[:min(msg.Cipher.SizeBlock, ta.Digest.Size)], msg.Encrypted) + + return xd(plain) +} + +// EncryptOnClient encrypts msg's plain bytes with a decryption key used by the server. +func (msg *MessageTraitCrypt) EncryptOnClient(sk *StaticKey, ta *MessageTraitAuth, xe func() []byte) error { + if sk == nil || msg.Cipher == nil || ta.Digest == nil || len(ta.HMAC) != ta.Digest.Size { + return ErrInvalidCryptPrerequisites + } + + plain := xe() + + msg.Encrypted = msg.Cipher.EncryptOnClient(sk, ta.HMAC[:min(msg.Cipher.SizeBlock, ta.Digest.Size)], plain) + if len(plain) != len(msg.Encrypted) { + return ErrInvalidEncryptedLength + } + + return nil +} + +// EncryptOnServer encrypts msg's plain bytes with a decryption key used by the client. +func (msg *MessageTraitCrypt) EncryptOnServer(sk *StaticKey, ta *MessageTraitAuth, xe func() []byte) error { + if sk == nil || msg.Cipher == nil || ta.Digest == nil || len(ta.HMAC) != ta.Digest.Size { + return ErrInvalidCryptPrerequisites + } + + plain := xe() + + msg.Encrypted = msg.Cipher.EncryptOnServer(sk, ta.HMAC[:min(msg.Cipher.SizeBlock, ta.Digest.Size)], plain) + if len(plain) != len(msg.Encrypted) { + return ErrInvalidEncryptedLength + } + + return nil +} + +// MessageTraitReplay contains fields and implements features related to replay protection. +type MessageTraitReplay struct { + // ReplayPacketID is a 4-byte packet ID used for replay protection. + ReplayPacketID uint32 + // ReplayTimestamp is a 4-byte timestamp used for replay protection. + ReplayTimestamp uint32 +} + +// ValidateReplayTimestamp returns true if msg has a valid ReplayTimestamp (relative to the provided time). +func (msg *MessageTraitReplay) ValidateReplayTimestamp(t time.Time) bool { + tReplay := time.Unix(int64(msg.ReplayTimestamp), 0) + tNowLow := t.UTC().Add(-TimestampValidationInterval) + tNowHigh := t.UTC().Add(TimestampValidationInterval) + return tReplay.After(tNowLow) && tReplay.Before(tNowHigh) +} + +// MetaData is an optional set of Type and Payload that may be included into WrappedKey. +type MetaData struct { + // Payload contains a number of bytes of variable length corresponding to Type. + Payload []byte + // Type indicates how to parse Payload correctly. It may equal 0x00 for any user defined data + // and 0x01 for a 4-byte timestamp which enables discarding old client keys on the server. + Type uint8 +} + +// WrappedKey is an authenticated and encrypted client key used to encrypt MessageCrypt in the crypt2 mode. +type WrappedKey struct { + MetaData + StaticKey + MessageTraitAuth + MessageTraitCrypt +} + +// Authenticate returns true if wk's HMAC is valid. +func (wk *WrappedKey) Authenticate(ad *AuthDigest, sk *StaticKey) bool { + return wk.MessageTraitAuth.AuthenticateOnClient(ad, sk, wk.ToBytesAuth) +} + +// DecryptAndAuthenticate decrypts wk's encrypted bytes before calling Authenticate. +func (wk *WrappedKey) DecryptAndAuthenticate(ad *AuthDigest, sk *StaticKey) bool { + if len(wk.Encrypted) < StaticKeyBytesTotal || + len(wk.Encrypted) > WrappedKeyBytesMax-LengthBytesTotal-CryptHMACBytesTotal || + wk.MessageTraitCrypt.DecryptOnClient(sk, &wk.MessageTraitAuth, wk.FromBytesCrypt) != nil { + return false + } + return wk.Authenticate(ad, sk) +} + +// EncryptAndSign encrypts wk's plain bytes before calling Sign. +func (wk *WrappedKey) EncryptAndSign(ad *AuthDigest, sk *StaticKey) error { + if err := wk.MessageTraitCrypt.EncryptOnServer(sk, &wk.MessageTraitAuth, wk.ToBytesCrypt); err != nil { + return err + } + return wk.Sign(ad, sk) +} + +// FromBase64 fills wk's internal structures (including decrypted StaticKey bytes) from a base64 string. +func (wk *WrappedKey) FromBase64(s string) error { + src, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return err + } + + if len(src) < WrappedKeyBytesMin+StaticKeyBytesTotal || len(src) > WrappedKeyBytesMax+StaticKeyBytesTotal { + return ErrInvalidSourceLength + } + + wk.StaticKey.KeyBytes = src[:StaticKeyBytesTotal] + + return wk.FromBytes(src[StaticKeyBytesTotal:]) +} + +// FromBytes fills wk's internal structures from a slice of bytes. +func (wk *WrappedKey) FromBytes(src []byte) error { + // Any WrappedKey has a 2-byte length at the end. Ensure that it has a valid value. + if len(src) < WrappedKeyBytesMin || len(src) > WrappedKeyBytesMax || + len(src) != int(BytesOrder.Uint16(src[len(src)-LengthBytesTotal:])) { + return ErrInvalidSourceLength + } + + // Any WrappedKey has a 32-byte SHA-256 HMAC at the beginning. + wk.Digest, wk.HMAC = AuthDigestDefault, src[:CryptHMACBytesTotal] + + // Any WrappedKey has an Encrypted part of at least 256 bytes between HMAC and Length. + wk.Cipher, wk.Encrypted = CryptCipherDefault, src[CryptHMACBytesTotal:len(src)-LengthBytesTotal] + + return nil +} + +// FromBytesCrypt fills wk's internal structures from a slice of bytes after decryption. +func (wk *WrappedKey) FromBytesCrypt(plain []byte) error { + if len(plain) != len(wk.Encrypted) { + return ErrInvalidPlainLength + } + + // Any WrappedKey has a StaticKey at the beginning of the plain text. + wk.StaticKey.KeyBytes = plain[:StaticKeyBytesTotal] + + // Any WrappedKey may have a MetaData.Type between StaticKey and MetaData in the plain text. + if len(plain) > StaticKeyBytesTotal { + wk.MetaData.Type = plain[StaticKeyBytesTotal] + } + + // Any KeyMata may have MetaData.Payload at the end of the plain text. + if len(plain) > StaticKeyBytesTotal+MetaDataTypeBytesTotal { + wk.MetaData.Payload = plain[StaticKeyBytesTotal+MetaDataTypeBytesTotal:] + } + + return nil +} + +// FromClientKeyFile fills wk's internal structures (including decrypted StaticKey bytes) from a given file. +func (wk *WrappedKey) FromClientKeyFile(path string) error { + err := wk.StaticKey.FromFile(path, StaticKeyFromFileBase64, 0, wk.StaticKey.FromBase64) + if err != nil { + return err + } + + if len(wk.StaticKey.KeyBytes) < WrappedKeyBytesMin || len(wk.StaticKey.KeyBytes) > WrappedKeyBytesMax { + return ErrInvalidStaticKeyFileContents + } + + err = wk.FromBytes(wk.StaticKey.KeyBytes[StaticKeyBytesTotal:]) + if err != nil { + return err + } + + wk.StaticKey.KeyBytes = wk.StaticKey.KeyBytes[:StaticKeyBytesTotal] + return nil +} + +// Sign computes and fills wk's HMAC. +func (wk *WrappedKey) Sign(ad *AuthDigest, sk *StaticKey) error { + return wk.MessageTraitAuth.SignOnClient(ad, sk, wk.ToBytesAuth) +} + +// ToBytes returns a slice of bytes representing wk's internal structures. +func (wk *WrappedKey) ToBytes() []byte { + dst := make([]byte, 0, len(wk.HMAC)+len(wk.Encrypted)+LengthBytesTotal) + dst = append(dst, wk.HMAC...) + dst = append(dst, wk.Encrypted...) + dst = BytesOrder.AppendUint16(dst, uint16(cap(dst))) + return dst +} + +// ToBytesAuth returns a slice of bytes representing wk's internal structures without HMAC. +func (wk *WrappedKey) ToBytesAuth() []byte { + if len(wk.MetaData.Payload) > 0 { + dst := make([]byte, 0, LengthBytesTotal+len(wk.StaticKey.KeyBytes)+MetaDataTypeBytesTotal+len(wk.MetaData.Payload)) + dst = BytesOrder.AppendUint16(dst, uint16(cap(dst)+CryptHMACBytesTotal)) + dst = append(dst, wk.StaticKey.KeyBytes...) + dst = append(dst, wk.MetaData.Type) + dst = append(dst, wk.MetaData.Payload...) + return dst + } else { + dst := make([]byte, 0, LengthBytesTotal+len(wk.StaticKey.KeyBytes)) + dst = BytesOrder.AppendUint16(dst, uint16(cap(dst)+CryptHMACBytesTotal)) + dst = append(dst, wk.StaticKey.KeyBytes...) + return dst + } +} + +// ToBytesCrypt returns a slice of bytes representing wk's internal structures before encryption. +func (wk *WrappedKey) ToBytesCrypt() []byte { + if len(wk.MetaData.Payload) > 0 { + dst := make([]byte, 0, len(wk.StaticKey.KeyBytes)+MetaDataTypeBytesTotal+len(wk.MetaData.Payload)) + dst = append(dst, wk.StaticKey.KeyBytes...) + dst = append(dst, wk.MetaData.Type) + dst = append(dst, wk.MetaData.Payload...) + return dst + } else { + return wk.StaticKey.KeyBytes + } +} + +var ( + BytesOrder = binary.BigEndian + + ErrInvalidAuthPrerequisites = errors.New("invalid auth prerequisites") + ErrInvalidCryptPrerequisites = errors.New("invalid crypt prerequisites") + ErrInvalidEncryptedLength = errors.New("invalid encrypted length") + ErrInvalidHeaderOpcode = errors.New("invalid header opcode") + ErrInvalidHMACLength = errors.New("invalid HMAC length") + ErrInvalidPlainLength = errors.New("invalid plain length") + ErrInvalidSourceLength = errors.New("invalid source length") + ErrMissingReusableHeader = errors.New("missing reusable header") +) + +const ( + /* + * Irrelevant MessageHeader.Opcode values: + * + * OpcodeControlHardResetClientV1 uint8 = 1 + * OpcodeControlHardResetServerV1 uint8 = 2 + * OpcodeControlSoftResetV1 uint8 = 3 + * OpcodeControlV1 uint8 = 4 + * OpcodeAckV1 uint8 = 5 + * OpcodeDataV1 uint8 = 6 + * OpcodeControlHardResetServerV2 uint8 = 8 + * OpcodeDataV2 uint8 = 9 + * OpcodeControlWrappedClientKeyV1 uint8 = 11 + */ + + OpcodeControlHardResetClientV2 uint8 = 7 + OpcodeControlHardResetClientV3 uint8 = 10 + + KeyIDMask uint8 = 0b1<