/* Copyright 2021 Gravitational, Inc. 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 client import ( "crypto/tls" "crypto/x509" "os" "path/filepath" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/api/identityfile" "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/sshutils" ) func TestLoadTLS(t *testing.T) { t.Parallel() // Load expected tls.Config. expectedTLSConfig := getExpectedTLSConfig(t) // Load TLSConfigCreds. creds := LoadTLS(expectedTLSConfig) // Build tls.Config and compare to expected tls.Config. tlsConfig, err := creds.TLSConfig() require.NoError(t, err) requireEqualTLSConfig(t, expectedTLSConfig, tlsConfig) // Load invalid tls.Config. invalidTLSCreds := LoadTLS(nil) _, err = invalidTLSCreds.TLSConfig() require.Error(t, err) _, err = invalidTLSCreds.SSHClientConfig() require.Error(t, err) } func TestLoadIdentityFile(t *testing.T) { t.Parallel() // Load expected tls.Config and ssh.ClientConfig. expectedTLSConfig := getExpectedTLSConfig(t) expectedSSHConfig := getExpectedSSHConfig(t) // Write identity file to disk. path := filepath.Join(t.TempDir(), "file") idFile := &identityfile.IdentityFile{ PrivateKey: keyPEM, Certs: identityfile.Certs{ TLS: tlsCert, SSH: sshCert, }, CACerts: identityfile.CACerts{ TLS: [][]byte{tlsCACert}, SSH: [][]byte{sshCACert}, }, } err := identityfile.Write(idFile, path) require.NoError(t, err) // Load identity file from disk. creds := LoadIdentityFile(path) // Build tls.Config and compare to expected tls.Config. tlsConfig, err := creds.TLSConfig() require.NoError(t, err) requireEqualTLSConfig(t, expectedTLSConfig, tlsConfig) // Build ssh.ClientConfig and compare to expected ssh.ClientConfig. sshConfig, err := creds.SSHClientConfig() require.NoError(t, err) requireEqualSSHConfig(t, expectedSSHConfig, sshConfig) // Load invalid identity. creds = LoadIdentityFile("invalid_path") _, err = creds.TLSConfig() require.Error(t, err) _, err = creds.SSHClientConfig() require.Error(t, err) } func TestLoadIdentityFileFromString(t *testing.T) { t.Parallel() // Load expected tls.Config and ssh.ClientConfig. expectedTLSConfig := getExpectedTLSConfig(t) expectedSSHConfig := getExpectedSSHConfig(t) // Write identity file to disk. path := filepath.Join(t.TempDir(), "file") idFile := &identityfile.IdentityFile{ PrivateKey: keyPEM, Certs: identityfile.Certs{ TLS: tlsCert, SSH: sshCert, }, CACerts: identityfile.CACerts{ TLS: [][]byte{tlsCACert}, SSH: [][]byte{sshCACert}, }, } err := identityfile.Write(idFile, path) require.NoError(t, err) b, err := os.ReadFile(path) require.NoError(t, err) // Load identity file from disk. creds := LoadIdentityFileFromString(string(b)) // Build tls.Config and compare to expected tls.Config. tlsConfig, err := creds.TLSConfig() require.NoError(t, err) requireEqualTLSConfig(t, expectedTLSConfig, tlsConfig) // Build ssh.ClientConfig and compare to expected ssh.ClientConfig. sshConfig, err := creds.SSHClientConfig() require.NoError(t, err) requireEqualSSHConfig(t, expectedSSHConfig, sshConfig) // Load invalid identity. creds = LoadIdentityFileFromString("invalid_creds") _, err = creds.TLSConfig() require.Error(t, err) _, err = creds.SSHClientConfig() require.Error(t, err) } func TestLoadKeyPair(t *testing.T) { t.Parallel() // Load expected tls.Config. expectedTLSConfig := getExpectedTLSConfig(t) // Write key pair and CAs files from bytes. path := t.TempDir() + "username" certPath, keyPath, caPath := path+".crt", path+".key", path+".cas" err := os.WriteFile(certPath, tlsCert, 0600) require.NoError(t, err) err = os.WriteFile(keyPath, keyPEM, 0600) require.NoError(t, err) err = os.WriteFile(caPath, tlsCACert, 0600) require.NoError(t, err) // Load key pair from disk. creds := LoadKeyPair(certPath, keyPath, caPath) // Build tls.Config and compare to expected tls.Config. tlsConfig, err := creds.TLSConfig() require.NoError(t, err) requireEqualTLSConfig(t, expectedTLSConfig, tlsConfig) // Load invalid keypairs. invalidIdentityCreds := LoadKeyPair("invalid_path", "invalid_path", "invalid_path") _, err = invalidIdentityCreds.TLSConfig() require.Error(t, err) } func TestLoadProfile(t *testing.T) { t.Parallel() profileName := "proxy.example.com" t.Run("normal profile", func(t *testing.T) { t.Parallel() dir := t.TempDir() writeProfile(t, &profile.Profile{ WebProxyAddr: profileName + ":3080", SiteName: "example.com", Username: "testUser", Dir: dir, }) testProfileContents(t, dir, profileName) }) t.Run("non existent profile", func(t *testing.T) { t.Parallel() // Load non existent profile. creds := LoadProfile("invalid_dir", "invalid_name") _, err := creds.TLSConfig() require.Error(t, err) _, err = creds.SSHClientConfig() require.Error(t, err) _, err = creds.Dialer(Config{}) require.Error(t, err) }) } func testProfileContents(t *testing.T, dir, name string) { // Load expected tls.Config and ssh.ClientConfig. expectedTLSConfig := getExpectedTLSConfig(t) expectedSSHConfig := getExpectedSSHConfig(t) // Load profile from disk. creds := LoadProfile(dir, name) // Build tls.Config and compare to expected tls.Config. tlsConfig, err := creds.TLSConfig() require.NoError(t, err) requireEqualTLSConfig(t, expectedTLSConfig, tlsConfig) // Build ssh.ClientConfig and compare to expected ssh.ClientConfig. sshConfig, err := creds.SSHClientConfig() require.NoError(t, err) requireEqualSSHConfig(t, expectedSSHConfig, sshConfig) // Build Dialer _, err = creds.Dialer(Config{}) require.NoError(t, err) } func writeProfile(t *testing.T, p *profile.Profile) { // Save profile and keys to disk. require.NoError(t, p.SaveToDir(p.Dir, true)) require.NoError(t, os.MkdirAll(p.KeyDir(), 0700)) require.NoError(t, os.MkdirAll(p.ProxyKeyDir(), 0700)) require.NoError(t, os.MkdirAll(p.TLSClusterCASDir(), 0700)) require.NoError(t, os.WriteFile(p.UserKeyPath(), keyPEM, 0600)) require.NoError(t, os.WriteFile(p.TLSCertPath(), tlsCert, 0600)) require.NoError(t, os.WriteFile(p.TLSCAPathCluster(p.SiteName), tlsCACert, 0600)) require.NoError(t, os.WriteFile(p.KnownHostsPath(), sshCACert, 0600)) require.NoError(t, os.MkdirAll(p.SSHDir(), 0700)) require.NoError(t, os.WriteFile(p.SSHCertPath(), sshCert, 0600)) require.NoError(t, os.WriteFile(p.PPKFilePath(), ppkFile, 0600)) } func getExpectedTLSConfig(t *testing.T) *tls.Config { cert, err := tls.X509KeyPair(tlsCert, keyPEM) require.NoError(t, err) pool := x509.NewCertPool() require.True(t, pool.AppendCertsFromPEM(tlsCACert)) return configureTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: pool, }) } func getExpectedSSHConfig(t *testing.T) *ssh.ClientConfig { cert, err := sshutils.ParseCertificate(sshCert) require.NoError(t, err) priv, err := keys.ParsePrivateKey(keyPEM) require.NoError(t, err) config, err := sshutils.ProxyClientSSHConfig(cert, priv, sshCACert) require.NoError(t, err) return config } func requireEqualTLSConfig(t *testing.T, expected *tls.Config, actual *tls.Config) { require.Empty(t, cmp.Diff(expected, actual, cmpopts.IgnoreFields(tls.Config{}, "GetClientCertificate"), cmpopts.IgnoreUnexported(tls.Config{}, x509.CertPool{}), )) } func requireEqualSSHConfig(t *testing.T, expected *ssh.ClientConfig, actual *ssh.ClientConfig) { require.Empty(t, cmp.Diff(expected, actual, cmpopts.IgnoreFields(ssh.ClientConfig{}, "Auth", "HostKeyCallback"), )) } var ( tlsCert = []byte(`-----BEGIN CERTIFICATE----- MIIDyzCCArOgAwIBAgIQD3MiJ2Au8PicJpCNFbvcETANBgkqhkiG9w0BAQsFADBe MRQwEgYDVQQKEwtleGFtcGxlLmNvbTEUMBIGA1UEAxMLZXhhbXBsZS5jb20xMDAu BgNVBAUTJzIwNTIxNzE3NzMzMTIxNzQ2ODMyNjA5NjAxODEwODc0NTAzMjg1ODAe Fw0yMTAyMTcyMDI3MjFaFw0yMTAyMTgwODI4MjFaMIGCMRUwEwYDVQQHEwxhY2Nl c3MtYWRtaW4xCTAHBgNVBAkTADEYMBYGA1UEEQwPeyJsb2dpbnMiOm51bGx9MRUw EwYDVQQKEwxhY2Nlc3MtYWRtaW4xFTATBgNVBAMTDGFjY2Vzcy1hZG1pbjEWMBQG BSvODwEHEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAM5FFaCeK59lwIthyXgSCMZbHTDxsy66Cbm/XhwFbKQLngyS0oKkHbh06INN UfTAAEaFlMG0CzdAyGyRSu9FK8BE127kRHBs6hb1pTgy2f6TFkFo/h4WTWW4GQSi O8Al7A2tuRjc3mAnk71q+kvpQYS7tnkhmFCYE8jKxMtlYG39x4kQ6btll7P9zI6X Zv5RRrlzqADuwZpEcLYVi0TjITqPbx3rDZT4l+EmslhaoG+xE5Vu+GYXLlvwB9E/ amfN1Z9Kps4Ob6Jxxse9kjeMir9mwiNkBWVyhH/LETDA9Xa6sTQ2e75MYM7yXJLY OmBKV4g176Qf1T1ye7a/Ggn4t2UCAwEAAaNgMF4wDgYDVR0PAQH/BAQDAgWgMB0G A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB8GA1Ud IwQYMBaAFJWqMooE05nf263F341pOO+mPMSqMA0GCSqGSIb3DQEBCwUAA4IBAQCK s0yPzkSuCY/LFeHJoJeNJ1SR+EKbk4zoAnD0nbbIsd2quyYIiojshlfehhuZE+8P bzpUNG2aYKq+8lb0NO+OdZW7kBEDWq7ZwC8OG8oMDrX385fLcicm7GfbGCmZ6286 m1gfG9yqEte7pxv3yWM+7X2bzEjCBds4feahuKPNxOAOSfLUZiTpmOVlRzrpRIhu 2XxiuH+E8n4AP8jf/9bGvKd8PyHohtHVf8HWuKLZxWznQhoKkcfmUmlz5q8ci4Bq WQdM2NXAMABGAofGrVklPIiraUoHzr0Xxpia4vQwRewYXv8bCPHW+8g8vGBGvoG2 gtLit9DL5DR5ac/CRGJt -----END CERTIFICATE-----`) keyPEM = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAzkUVoJ4rn2XAi2HJeBIIxlsdMPGzLroJub9eHAVspAueDJLS gqQduHTog01R9MAARoWUwbQLN0DIbJFK70UrwETXbuREcGzqFvWlODLZ/pMWQWj+ HhZNZbgZBKI7wCXsDa25GNzeYCeTvWr6S+lBhLu2eSGYUJgTyMrEy2Vgbf3HiRDp u2WXs/3Mjpdm/lFGuXOoAO7BmkRwthWLROMhOo9vHesNlPiX4SayWFqgb7ETlW74 ZhcuW/AH0T9qZ83Vn0qmzg5vonHGx72SN4yKv2bCI2QFZXKEf8sRMMD1drqxNDZ7 vkxgzvJcktg6YEpXiDXvpB/VPXJ7tr8aCfi3ZQIDAQABAoIBAE1Vk207wAksAgt/ 5yQwRr/vizs9czuSnnDYsbT5x6idfm0iYvB+DXKJyl7oD1Ee5zuJe6NAGHBnxn0F 4D1jBqs4ZDj8NjicbQucn4w5bIfIp7BwZ83p+KypYB/fn11EGoNqXZpXvLv6Oqbq w9rQIjNcmWZC1TNqQQioFS5Y3NV/gw5uYCRXZlSLMsRCvcX2+LN2EP76ZbkpIVpT CidC2TxwFPPbyMsG774Olfz4U2IDgX1mO+milF7RIa/vPADSeHAX6tJHmZ13GsyP 0GAdPbFa0Ls/uykeGi1uGPFkdkNEqbWlDf1Z9IG0dr/ck2eh8G2X8E+VFgzsKp4k WtH9nGECgYEA53lFodLiKQjQR7IoUmGp+P6qnrDwOdU1RfT9jse35xOb9tYvZs3X kUXU+MEGAMW1Pvmo1v9xOjZbdFYB9I/tIYTSyjYQNaFjgJMPMLSx2qjMzhFXAY5f 8t20/CBt2V1q46aa8tR2ll//QvY4mqvJUaaB0pkuasFbKMXJcGKdvdkCgYEA5CAo UI8NVA9GqAJfs7hkGHQwpX1X1+JpFhF4dZKsV40NReqaK0vd/mWTYjlMOPO6oolr PoCDUlQYU6poIDtEnfJ6KkYuLMgxZKnS2OlDthKoZJe9aUTCP1RhTVHyyABRXbGg tNMKFYkZ38C9+JM+X5T0eKZTHeK+wjiZd55+sm0CgYAmyp0PxI6gP9jf2wyE2dcp YkxnsdFgb8mwwqDnl7LLJ+8gS76/5Mk2kFRjp72AzaFVP3O7LC3miouDEJLdUG12 C5NjzfGjezt4payLBg00Tsub0S4alaigw+T7x9eA8PXj1tzqyw5gnw/hQfA0g4uG gngJOiCcRXEogRUEH5K96QKBgFUnB8ViUHhTJ22pTS3Zo0tZe5saWYLVGaLKLKu+ byRTG2RAuQF2VUwTgFtGxgPwPndTUjvHXr2JdHcugaWeWfOXQjCrd6rxozZPCcw7 7jF1b3P1DBfSOavIBHYHI9ex/q05k6JLsFTvkz/pQ0AZPkwRXtv2QcpDDC+VTvvO pr5VAoGBAJBhNjs9wAu+ZoPcMZcjIXT/BAj2tQYiHoRnNpvQjDYbQueUBeI0Ry8d 5QnKS2k9D278P6BiDBz1c+fS8UErOxY6CS0pi4x3fjMliPwXj/w7AzjlXgDBhRcp 90Ns/9SamlBo9j8ETm9g9D3EVir9zF5XvoR13OdN9gabGy1GuubT -----END RSA PRIVATE KEY-----`) tlsCACert = []byte(`-----BEGIN CERTIFICATE----- MIIDiTCCAnGgAwIBAgIRAJlp/39yg8U604bjsxgcoC0wDQYJKoZIhvcNAQELBQAw XjEUMBIGA1UEChMLZXhhbXBsZS5jb20xFDASBgNVBAMTC2V4YW1wbGUuY29tMTAw LgYDVQQFEycyMDM5MjIyNTY2MzcxMDQ0NDc3MzYxNjA0MTk0NjU2MTgzMDA5NzMw HhcNMjEwMjAzMDAyOTQ2WhcNMzEwMjAxMDAyOTQ2WjBeMRQwEgYDVQQKEwtleGFt cGxlLmNvbTEUMBIGA1UEAxMLZXhhbXBsZS5jb20xMDAuBgNVBAUTJzIwMzkyMjI1 NjYzNzEwNDQ0NzczNjE2MDQxOTQ2NTYxODMwMDk3MzCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBAKnIJmcKgzj/FbvF6/OYkw3owsS3XU6AcJZ7HmTfYpZF ozqTDVJdHMFQVfu6cp/6hkzoZ/t7hKT6Nd/O2mlIZdBCfT5ZKESRvTGAeCUANKA5 /D4+6PDdW6AutOFUGbHQ1nYLB7HRgaXF/aZmzFPsPNwX8Wm8EByL+Dws61EmSBBv Soado5rPG78mAnRpFvyYbzBDkxzsgLIfv0EPw9jhSjrT3OVjCXnBv53u2S+UbJfR jmI7MutjNbJ/rIBp7JpRHJASmW7oj65WPH0SE0+67XwXYKbs0b7CcSuYW+1S+l9R uGswW4hqwMloP9sTZoWzgT+nCXQSYUavQF+UJZ/dklMCAwEAAaNCMEAwDgYDVR0P AQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFC4555Otcq4GAYcc QQDJgh1TKrFvMA0GCSqGSIb3DQEBCwUAA4IBAQBYsEMJYmSD6Dc1suUEnkWo7kOw va/aaOu0Phy9SK3hCjg+tatHVVDHO2dZdVCAvCe36BcLiZL1ovFZAXzEzOovwLx/ AVjXpMXTJj52RSMOAtRVSkk3/WOHrGOGIBW2bCKxF4ORXJfWJrdtaObwPPV5sbDC ACdlNMujdBfUM8EDNmvREI/sVmqL6FK9l6elO/bWLJoiaRTxI+CMixpfIYq8pAwJ UpgZGjcwco4eqXm7rgbQ4wLaMU6hyk8OE5Glk5E6qpnbVzlrL/jl2iE6EqvI6GJn Na6B0YR7mdrrL+lyzymnOr6UOrT5nUWRAB1QeY7dhBNnsvoZwaS3VLSc1KCk -----END CERTIFICATE-----`) sshCert = []byte("ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg8C10PShw+GxCadSlC4nFURIAyvDtgWRvHPabpL5wzDQAAAADAQABAAABAQDORRWgniufZcCLYcl4EgjGWx0w8bMuugm5v14cBWykC54MktKCpB24dOiDTVH0wABGhZTBtAs3QMhskUrvRSvARNdu5ERwbOoW9aU4Mtn+kxZBaP4eFk1luBkEojvAJewNrbkY3N5gJ5O9avpL6UGEu7Z5IZhQmBPIysTLZWBt/ceJEOm7ZZez/cyOl2b+UUa5c6gA7sGaRHC2FYtE4yE6j28d6w2U+JfhJrJYWqBvsROVbvhmFy5b8AfRP2pnzdWfSqbODm+iccbHvZI3jIq/ZsIjZAVlcoR/yxEwwPV2urE0Nnu+TGDO8lyS2DpgSleINe+kH9U9cnu2vxoJ+LdlAAAAAAAAAAAAAAABAAAADGFjY2Vzcy1hZG1pbgAAABAAAAAMYWNjZXNzLWFkbWluAAAAAGAtfCkAAAAAYC4lJQAAAAAAAACdAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnRlbGVwb3J0LXJvbGVzAAAALQAAACl7InZlcnNpb24iOiJ2MSIsInJvbGVzIjpbImFjY2Vzcy1hZG1pbiJdfQAAAA90ZWxlcG9ydC10cmFpdHMAAAATAAAAD3sibG9naW5zIjpudWxsfQAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQD3VbuNmR0h3tjYIkTVG+HfNByigp6tuNl8XVylIWx7a7ojRA1nJVzAtNs9QQMut8XY+7jxf4Ue83eIaE0e0QKA0GZlRdbSG0zaYzK8CDAcPVN6Ywt8jnGKuuMhBAckGkN/9nyuJHgTAKeHYgdgQgijPuW/D59s3Sk3vCRHryZzJfZDQ52i40B1q2zLvCcQa6UBvPblHAF3usRa08DnsNkgLey1EkkyvBazqt1amH2Epl3uJRHHUtRVSp2a+0597leT58RZNFfFfB9pccPJfD7cn+iiDmN62T/8YslLYl/O6xCJ43Or7wIRHwJ1tY5hq/Bw7LYn29zeBrIkxIvsH8WtAAABFAAAAAxyc2Etc2hhMi01MTIAAAEAhIz0X+wgA0B8Bi67ALpTEA3kHVWaQY3aT+Ig8obof9upq51H0YlySPJph8h6pVzfSJzQYtuGbmzQ/XAGRMn541mnSUGoy0WCHzscyCowaj9VgjFyVpct7Nz98dB3PnRocNTajGGla+AteZEU3d6KXv/CaA4NGwO3k0rYB+UfX0AAaatAwwxnzYehpCvwSqPdrq/OIyb0aljZHADoNRrcnmYDbB1V76WWY6eTCxYGXx1QyU4A8kH9U8pIZ1fVif/i8dSTbBTftTtv5bmO4WUbVscRw/xIqgZ8v6StNLGHPTt/+Zn+iUoiIrwcnpy+yQp2SRTv7+Lg2SSvJO818x3NNg==") sshCACert = []byte("@cert-authority *.example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMIgxZpT5362npj0x6NQA76IB73bcK85K8cEyKURuHtFC83RjBzvzqtUz6X02+6ohVZiR2MdmsXkCLznzwEIZ0NtoxgnLTZLmduPLeAuYW2vIFpd0G17y6Yog9vxhQ0BLdlhU5Y3JYjRYjmQMfe1iD/RXWD6rEvgWlz+c3HMQR33JqkVIEFH34upfkC2RQG3TXjMe5t14l3yCTtyF5YGzN7+6z/4+/EDto/F3zVtSEp+k8XE/m0ddTGo7usa8ErAom31RwrgkNRmgJmPleDwEflybEsgGKApJXkfFxmG2wu20JoEt/CFjY3fIIa/5aqIGJPpMH4aEdLcj/iyNCog8D type=host") ppkFile = []byte(`PuTTY-User-Key-File-3: ssh-rsa Encryption: none Comment: test.com Public-Lines: 6 AAAAB3NzaC1yc2EAAAADAQABAAABAQDORRWgniufZcCLYcl4EgjGWx0w8bMuugm5 v14cBWykC54MktKCpB24dOiDTVH0wABGhZTBtAs3QMhskUrvRSvARNdu5ERwbOoW 9aU4Mtn+kxZBaP4eFk1luBkEojvAJewNrbkY3N5gJ5O9avpL6UGEu7Z5IZhQmBPI ysTLZWBt/ceJEOm7ZZez/cyOl2b+UUa5c6gA7sGaRHC2FYtE4yE6j28d6w2U+Jfh JrJYWqBvsROVbvhmFy5b8AfRP2pnzdWfSqbODm+iccbHvZI3jIq/ZsIjZAVlcoR/ yxEwwPV2urE0Nnu+TGDO8lyS2DpgSleINe+kH9U9cnu2vxoJ+Ld Private-Lines: 14 AAABAE1Vk207wAksAgt/5yQwRr/vizs9czuSnnDYsbT5x6idfm0iYvB+DXKJyl7o D1Ee5zuJe6NAGHBnxn0F4D1jBqs4ZDj8NjicbQucn4w5bIfIp7BwZ83p+KypYB/f n11EGoNqXZpXvLv6Oqbqw9rQIjNcmWZC1TNqQQioFS5Y3NV/gw5uYCRXZlSLMsRC vcX2+LN2EP76ZbkpIVpTCidC2TxwFPPbyMsG774Olfz4U2IDgX1mO+milF7RIa/v PADSeHAX6tJHmZ13GsyP0GAdPbFa0Ls/uykeGi1uGPFkdkNEqbWlDf1Z9IG0dr/c k2eh8G2X8E+VFgzsKp4kWtH9nGEAAACBAOQgKFCPDVQPRqgCX7O4ZBh0MKV9V9fi aRYReHWSrFeNDUXqmitL3f5lk2I5TDjzuqKJaz6Ag1JUGFOqaCA7RJ3yeipGLizI MWSp0tjpQ7YSqGSXvWlEwj9UYU1R8sgAUV2xoLTTChWJGd/AvfiTPl+U9HimUx3i vsI4mXeefrJtAAAAgQDneUWh0uIpCNBHsihSYan4/qqesPA51TVF9P2Ox7fnE5v2 1i9mzdeRRdT4wQYAxbU++ajW/3E6Nlt0VgH0j+0hhNLKNhA1oWOAkw8wtLHaqMzO EVcBjl/y3bT8IG3ZXWrjppry1HaWX/9C9jiaq8lRpoHSmS5qwVsoxclwYp292QAA AIBV1ZA8WqvC+xZrPwmtmN87BHwGjqpE52kbUfcD94k8IqqhPR9oN9uOlcoBzZiS 3SkunUpmzKlcXe63RQYOEqEVlTNOafcYNc5gW8NXKrgF7vBE91VsfmOGJvLt3pIv k53lH1qmEOm9+vrhNwNzpHk4AqDkP+0YDG++B4n0BtJJpw== Private-MAC: 8951bbe929e0714a61df01bc8fbc5223e3688f174aee29339931984fb9224c7d`) )