Skip to content

Commit d1179ca

Browse files
authored
STAC-22970: Validate HTTPS endpoints protected by self-signed certificates or signed with private CA (#103)
* STAC-22970: Validate HTTPS endpoints protected by self-signed certificates or signed with private CA * STAC-22970: Address GitGuardian finding in a test file
1 parent caf654a commit d1179ca

File tree

15 files changed

+660
-125
lines changed

15 files changed

+660
-125
lines changed

cmd/context/context_delete_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,5 @@ func TestDeleteContext(t *testing.T) {
3737
cfg, err := config.ReadConfig(cli.ConfigPath)
3838
assert.NoError(t, err)
3939

40-
assert.Equal(t, 2, len(cfg.Contexts))
40+
assert.Equal(t, 5, len(cfg.Contexts))
4141
}

cmd/context/context_save.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package context
22

33
import (
4+
"encoding/base64"
5+
"os"
6+
47
"github.com/spf13/cobra"
58
stscobra "github.com/stackvista/stackstate-cli/internal/cobra"
69
"github.com/stackvista/stackstate-cli/internal/common"
@@ -13,12 +16,15 @@ const (
1316
)
1417

1518
type SaveArgs struct {
16-
Name string
17-
URL string
18-
APIToken string
19-
ServiceToken string
20-
APIPath string
21-
SkipValidate bool
19+
Name string
20+
URL string
21+
APIToken string
22+
ServiceToken string
23+
APIPath string
24+
CaCertPath string
25+
CaCertBase64Data string
26+
SkipValidate bool
27+
SkipSSLFlag bool
2228
}
2329

2430
func SaveCommand(cli *di.Deps) *cobra.Command {
@@ -36,7 +42,9 @@ func SaveCommand(cli *di.Deps) *cobra.Command {
3642
cmd.Flags().StringVar(&args.ServiceToken, common.ServiceTokenFlag, "", common.ServiceTokenFlagUse)
3743
cmd.Flags().StringVar(&args.APIPath, APIPathFlag, "/api", "Specify the path of the API end-point, e.g. the part that comes after the URL")
3844
cmd.Flags().BoolVar(&args.SkipValidate, "skip-validate", false, "Skip validation of the context")
39-
45+
cmd.Flags().StringVar(&args.CaCertPath, common.CaCertPathFlag, "", common.CaCertPathFlagUse)
46+
cmd.Flags().StringVar(&args.CaCertBase64Data, common.CaCertBase64DataFlag, "", common.CaCertBase64DataFlagUse)
47+
cmd.Flags().BoolVar(&args.SkipSSLFlag, common.SkipSSLFlag, false, common.SkipSSLFlagUse)
4048
cmd.MarkFlagRequired(common.URLFlag) //nolint:errcheck
4149
stscobra.MarkMutexFlags(cmd, []string{common.APITokenFlag, common.ServiceTokenFlag, common.K8sSATokenFlag}, "tokens", true)
4250

@@ -57,8 +65,23 @@ func RunContextSaveCommand(args *SaveArgs) func(cli *di.Deps, cmd *cobra.Command
5765
APIToken: args.APIToken,
5866
ServiceToken: args.ServiceToken,
5967
APIPath: args.APIPath,
68+
SkipSSL: args.SkipSSLFlag,
6069
},
6170
}
71+
// Use private CA only if SkipSSL is not enabled
72+
if !args.SkipSSLFlag {
73+
// Providing CA certificate from file takes precedence over providing from the command line argument.
74+
if args.CaCertPath != "" {
75+
data, serr := os.ReadFile(args.CaCertPath)
76+
if serr != nil {
77+
return common.NewReadFileError(serr, args.CaCertPath)
78+
}
79+
namedCtx.Context.CaCertBase64Data = base64.StdEncoding.EncodeToString(data)
80+
namedCtx.Context.CaCertPath = ""
81+
} else if args.CaCertBase64Data != "" {
82+
namedCtx.Context.CaCertBase64Data = args.CaCertBase64Data
83+
}
84+
}
6285

6386
if !args.SkipValidate {
6487
if _, err := ValidateContext(cli, cmd, namedCtx.Context); err != nil {

cmd/context/context_save_test.go

Lines changed: 138 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package context
33
import (
44
"testing"
55

6+
"github.com/stretchr/testify/require"
7+
68
"github.com/spf13/cobra"
79
"github.com/stackvista/stackstate-cli/internal/config"
810
"github.com/stackvista/stackstate-cli/internal/di"
@@ -16,50 +18,144 @@ func setupSaveCmd(t *testing.T) (*di.MockDeps, *cobra.Command) {
1618
return &cli, cmd
1719
}
1820

19-
func TestSaveNewContext(t *testing.T) {
20-
cli, cmd := setupSaveCmd(t)
21-
setupConfig(t, cli)
22-
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--name", "baz", "--url", "http://baz.com", "--api-token", "my-token")
23-
assert.NoError(t, err)
24-
25-
cfg, err := config.ReadConfig(cli.ConfigPath)
26-
assert.NoError(t, err)
27-
assert.Equal(t, "baz", cfg.CurrentContext)
28-
assert.Len(t, cfg.Contexts, 4)
29-
30-
validateContext(t, cfg, cfg.CurrentContext, "http://baz.com", "my-token", "", "", "/api")
31-
}
32-
33-
func TestSaveExistingContext(t *testing.T) {
34-
cli, cmd := setupSaveCmd(t)
35-
setupConfig(t, cli)
21+
func TestSaveContext(t *testing.T) { //nolint:funlen
22+
tests := []struct {
23+
name string
24+
args []string
25+
expectedContext config.NamedContext
26+
totalContextInConfig int
27+
wantErr bool
28+
errorMessage string
29+
}{
30+
{
31+
name: "new context",
32+
args: []string{"--name", "baz", "--url", "http://baz.com", "--api-token", "my-token"},
33+
expectedContext: config.NamedContext{
34+
Name: "baz",
35+
Context: &config.StsContext{
36+
URL: "http://baz.com",
37+
APIToken: "my-token",
38+
APIPath: "/api",
39+
},
40+
},
41+
totalContextInConfig: 7,
42+
wantErr: false,
43+
},
44+
{
45+
name: "existing context",
46+
args: []string{"--name", "bar", "--url", "http://bar.com", "--service-token", "my-token"},
47+
expectedContext: config.NamedContext{
48+
Name: "bar",
49+
Context: &config.StsContext{
50+
URL: "http://bar.com",
51+
ServiceToken: "my-token",
52+
APIPath: "/api",
53+
},
54+
},
55+
totalContextInConfig: 6,
56+
wantErr: false,
57+
},
58+
{
59+
name: "existing context ca-cert is set with ca-cert-path",
60+
args: []string{"--name", "bar", "--url", "http://bar.com", "--service-token", "my-token", "--ca-cert-path", "testdata/selfSignedCert.crt"},
61+
expectedContext: config.NamedContext{
62+
Name: "bar",
63+
Context: &config.StsContext{
64+
URL: "http://bar.com",
65+
ServiceToken: "my-token",
66+
APIPath: "/api",
67+
CaCertBase64Data: selfSignedBase64Cert,
68+
},
69+
},
70+
totalContextInConfig: 6,
71+
wantErr: false,
72+
},
73+
{
74+
name: "new context ca-cert is set with ca-cert-path",
75+
args: []string{"--name", "cacertdata", "--url", "http://bar.com", "--service-token", "my-token", "--ca-cert-base64-data", privateCaBase64Cert},
76+
expectedContext: config.NamedContext{
77+
Name: "cacertdata",
78+
Context: &config.StsContext{
79+
URL: "http://bar.com",
80+
ServiceToken: "my-token",
81+
APIPath: "/api",
82+
CaCertBase64Data: privateCaBase64Cert,
83+
},
84+
},
85+
totalContextInConfig: 7,
86+
wantErr: false,
87+
},
88+
{
89+
name: "ca-cert-path takes precedence over ca-cert-base64-data",
90+
args: []string{"--name", "cacertdata", "--url", "http://bar.com", "--service-token", "my-token", "--ca-cert-path", "testdata/selfSignedCert.crt", "--ca-cert-base64-data", privateCaBase64Cert},
91+
expectedContext: config.NamedContext{
92+
Name: "cacertdata",
93+
Context: &config.StsContext{
94+
URL: "http://bar.com",
95+
ServiceToken: "my-token",
96+
APIPath: "/api",
97+
CaCertBase64Data: selfSignedBase64Cert,
98+
},
99+
},
100+
totalContextInConfig: 7,
101+
wantErr: false,
102+
},
103+
{
104+
name: "ca-cert ignored if skip-ssl is set",
105+
args: []string{"--name", "bar", "--url", "http://bar.com", "--service-token", "my-token", "--skip-ssl", "--ca-cert-path", "/path/to/ca.crt", "--ca-cert-base64-data", "base64-data"},
106+
expectedContext: config.NamedContext{
107+
Name: "bar",
108+
Context: &config.StsContext{
109+
URL: "http://bar.com",
110+
ServiceToken: "my-token",
111+
APIPath: "/api",
112+
SkipSSL: true,
113+
CaCertBase64Data: "",
114+
CaCertPath: "",
115+
},
116+
},
117+
totalContextInConfig: 6,
118+
wantErr: false,
119+
},
120+
{
121+
name: "no save on missing tokens",
122+
args: []string{"--name", "bar", "--url", "http://my-bar.com"},
123+
expectedContext: config.NamedContext{},
124+
wantErr: true,
125+
errorMessage: "one of the required flags {api-token | service-token} not set",
126+
},
127+
{
128+
name: "ca-cert-path is not found",
129+
args: []string{"--name", "bar", "--url", "http://my-bar.com", "--service-token", "my-token", "--ca-cert-path", "/path/to/ca.crt"},
130+
expectedContext: config.NamedContext{},
131+
wantErr: true,
132+
errorMessage: "no such file or directory",
133+
},
134+
}
36135

37-
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--name", "bar", "--url", "http://bar.com", "--service-token", "my-token")
38-
assert.NoError(t, err)
39-
40-
cfg, err := config.ReadConfig(cli.ConfigPath)
41-
assert.NoError(t, err)
42-
assert.Equal(t, "bar", cfg.CurrentContext)
43-
assert.Len(t, cfg.Contexts, 3)
44-
validateContext(t, cfg, cfg.CurrentContext, "http://bar.com", "", "my-token", "", "/api")
136+
for _, tt := range tests {
137+
t.Run(tt.name, func(t *testing.T) {
138+
cli, cmd := setupSaveCmd(t)
139+
setupConfig(t, cli)
140+
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, tt.args...)
141+
if tt.wantErr {
142+
require.Error(t, err)
143+
if tt.errorMessage != "" {
144+
assert.Contains(t, err.Error(), tt.errorMessage)
145+
}
146+
} else {
147+
cfg, err := config.ReadConfig(cli.ConfigPath)
148+
assert.NoError(t, err)
149+
assert.Equal(t, tt.expectedContext.Name, cfg.CurrentContext)
150+
assert.Len(t, cfg.Contexts, tt.totalContextInConfig)
151+
validateContext(t, cfg, tt.expectedContext)
152+
}
153+
})
154+
}
45155
}
46156

47-
func validateContext(t *testing.T, cfg *config.Config, name string, url string, apiToken, serviceToken, k8sSAToken string, apiPath string) {
48-
ctx, err := cfg.GetContext(name)
157+
func validateContext(t *testing.T, cfg *config.Config, expectedContext config.NamedContext) {
158+
ctx, err := cfg.GetContext(expectedContext.Name)
49159
assert.NoError(t, err)
50-
assert.Equal(t, url, ctx.Context.URL)
51-
assert.Equal(t, apiToken, ctx.Context.APIToken)
52-
assert.Equal(t, serviceToken, ctx.Context.ServiceToken)
53-
assert.Equal(t, k8sSAToken, ctx.Context.K8sSAToken)
54-
assert.Equal(t, apiPath, ctx.Context.APIPath)
55-
}
56-
57-
func TestNoSaveOnMissingTokens(t *testing.T) {
58-
cli, cmd := setupSaveCmd(t)
59-
60-
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--name", "bar", "--url", "http://my-bar.com")
61-
assert.Errorf(t, err, "missing required argument: --api-token")
62-
63-
// Should not have written config file
64-
assert.NoFileExists(t, cli.ConfigPath)
160+
assert.Equal(t, expectedContext.Context, ctx.Context)
65161
}

cmd/context/test_helper.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,23 @@ import (
88
"github.com/stackvista/stackstate-cli/internal/di"
99
)
1010

11+
const (
12+
//nolint:lll
13+
selfSignedBase64Cert = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWRHZ0F3SUJBZ0lVVi9hSmoxZkVjQ2dOVTJGYWZZMHVSTHF5N21Bd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0tURW5NQ1VHQTFVRUF3d2VkbWxzYVdGcmIzWXVjMkZ1WkdKdmVDNXpkR0ZqYTNOMFlYUmxMbWx2TUNBWApEVEkxTURjeU1USXdORFUxTmxvWUR6SXhNalV3TmpJM01qQTBOVFUyV2pBcE1TY3dKUVlEVlFRRERCNTJhV3hwCllXdHZkaTV6WVc1a1ltOTRMbk4wWVdOcmMzUmhkR1V1YVc4d1hEQU5CZ2txaGtpRzl3MEJBUUVGQUFOTEFEQkkKQWtFQW9wUXVPSmZJa0xDV0pLVDcwaGdiSEpwVWtFQitaYTJwOXVBMUlOUktNNEFyN2RjVjltdXhOS09jSloycwpWdCtiK1lTS1c4cnRteE5QUVh1RTJENHRlUUlEQVFBQm80SE9NSUhMTUIwR0ExVWREZ1FXQkJRVTBPTFZRRzEyCndNb0VLSGdxSG1aeVhTelozekFmQmdOVkhTTUVHREFXZ0JRVTBPTFZRRzEyd01vRUtIZ3FIbVp5WFN6WjN6QVAKQmdOVkhSTUJBZjhFQlRBREFRSC9NSGdHQTFVZEVRUnhNRytDSG5acGJHbGhhMjkyTG5OaGJtUmliM2d1YzNSaApZMnR6ZEdGMFpTNXBiNElqYjNSc2NDMTJhV3hwWVd0dmRpNXpZVzVrWW05NExuTjBZV05yYzNSaGRHVXVhVytDCktHOTBiSEF0YUhSMGNDMTJhV3hwWVd0dmRpNXpZVzVrWW05NExuTjBZV05yYzNSaGRHVXVhVzh3RFFZSktvWkkKaHZjTkFRRUxCUUFEUVFBZllBVk1lTVJHbFcrR1prellPeGRIaVhYNEFISHA5SWxvWlBMbUJHNExtdlpDODBoVgpLNGNSVUVHSGtSeGdrMGgwYzl3RDhOZFZSM1FuRTBubjZXUEUKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
14+
//nolint:lll
15+
privateCaBase64Cert = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZiVENDQTFXZ0F3SUJBZ0lVVGdGVm56eFNpbGR6MC9VenQ2UVR0bGpyMWVZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JURUxNQWtHQTFVRUJoTUNRVlV4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTQpHRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpEQWdGdzB5TlRBM01qRXlNRFEzTlRCYUdBOHlNVEkxCk1EWXlOekl3TkRjMU1Gb3dSVEVMTUFrR0ExVUVCaE1DUVZVeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXgKSVRBZkJnTlZCQW9NR0VsdWRHVnlibVYwSUZkcFpHZHBkSE1nVUhSNUlFeDBaRENDQWlJd0RRWUpLb1pJaHZjTgpBUUVCQlFBRGdnSVBBRENDQWdvQ2dnSUJBSTRjbEJlRFNoeEpBZ09lWjIyaERiaUViTVArc1dtRCsxVTdlNkZqCjhRelVVMkFWRkdvWjAwbEdUSDlxZVN4T1ZDMittWlBmb3ZTcmR0S2xYYm9PdEV0TldBZmhxZ2twOGh1ODRZb2UKaWxLT3YybWYvS0N0SzBPeTVkNlEwK3FPb2RPZVlIYlBLQk9vVDUya1FZMWZYeFNlNG8zc0tyZFQ3eGRhUi8xYgpiSGVUeWxuZmlmV3d0NmNiVlpOb1IxYmZ6ZnJYdjhkYk94emVqNWJ3SlVCeDNiaFI0UHN4Tm9JRDUrVUZMeHdxCmhOT3FZMEhIcU13djN2clYwQ2ZnWWNkZmRWaVBZalJvejNNaTBDallMRllmeWQ2eDF4azM3RTZ5MnVXQVoxY2EKVXJjSGlORVp6c0sxQTd1Y1BLWDh5WTVjWkY5MHBUMmhHWnNGT2NjQmxyYTZQVVA5ZXFwTm1pYm1zbWNXdzBWQwp6WEswenpkMUVnMzRnWlplQjI5eko1MWJ0QlNoazZqc3pRaUFlSElEeWJnOEdzYWhob2NPUjhEd3dtL3Ezekw2CnRiY0ZKZS9TWDFrQTE2TFZHMzZMYTRnb3IrQ1E5b1Zxb3N0OU1sQzBvRktoUmpoYnM5ZGdSWlJ5TFhMMEZ0UysKTDJIQ0NyY2krcUpwT2hjSTZQMDhzR1owOWlBd3h2c1AzYjY5S0J0RVlFREJmL29QSVJWSmRBYithMnBocVc5QgpoUGFYVXpGOFQ5QkJLQzJHKytIeHlKcTU3QlQ3T3FpNXRQTW91ZlRMRXNiQlgrNkViTVZmOGV5SllONjFKak0xCmJMOUZ1MFkwNW9NRFFQcC83RWk0dGp3TFQ1S2VuWGJWbnZUN0s0aGo5MTlNbXpBbytOOWNWeklvOVZNMEF0U0gKbk52SEFnTUJBQUdqVXpCUk1CMEdBMVVkRGdRV0JCUk5ETFFMNnkvL213Wi83SEtEWEdIMnhwNHVqakFmQmdOVgpIU01FR0RBV2dCUk5ETFFMNnkvL213Wi83SEtEWEdIMnhwNHVqakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHCkNTcUdTSWIzRFFFQkN3VUFBNElDQVFBM1FsWThnM2NmdWJ3akRmWnpXbVowWWhBUEgwb2lXTkhZd25YOTQvY2sKaWRjQzUzblRuVC9yN3lnZlNsVk8wbllUelg2YS8rWXFXWFczT0ZBcXZUREZYVis2bVhTb3FWQ0ptRlAzUVh6TwpuTmthcmEzcWhIKzZHVVE2RnFVaEpza1hZNHdMT05FT2Q2T1VlNmcwZ1NTalZJUkVxVWVYeWZvYUlJR1owNVNhClNVRDRVQnczT0U4ZVhWaTIyWHVCaWpTMWVTRHd6a3RDdWc5MW9BeWVlUGRpSWp5UGNiMmVQdzMyZE1JcDZoYU4KR1lFMnNPR3l0aWtKTnBnbmNqR3RGdkRaSzFkaVNvQWxzM21FR3hjVTdXd05WMlFzN24vTGJqbUNENjQ1WXRFWgozVnJZNG10bEs1dEN3RURNcUFYK3ZScXJ2L09CL1R0Z3FvUG5HdmJkdWNoNThyMGIyUmtyZ3BtaUt1a3FxRUc3ClJiQmJNeWlSMXpjWmJoQm9SbnkxcXVEWm52MmxmVUJUdHVpV1JIUDNSRTRBNEIrYnp4bTI0UkVYZHRTSVUrUXAKaytZZjNuRGg5Y1Z2akpMWDZ5dmdmOUN5ZHIyQ2FVM015aTBCdmUyUnVJUm15VXlFYkE1MWUzV1F0NVF6emU2TApSS3A1a0JQR2ZjRTRTMmdDdi9DYktqQjV2V1doY2tieW9NL0pJMVFpSU94U1puOHFGWXg3NFdkMEJsYTNNaFhNClBOcXo3eDZxb3pWa1FzWTRBK1FEOUhnZE1Rbms3QlhKR01tbzJ3OSszdVB2SGJCdXJSV0FTMXRISVlUVlA0MVkKYXlXci9wTncwVWsrS2drMkdXQmx2T3VZSXMzN2RnMkw3RkNUeGR2bXU2dHNpK3QvUEpqcTNFWGkweDFzcE1aWAo5UT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
16+
)
17+
1118
func setupConfig(t *testing.T, cli *di.MockDeps) {
1219
cfg := &config.Config{
1320
CurrentContext: "foo",
1421
Contexts: []*config.NamedContext{
15-
{Name: "foo", Context: newContext("http://foo.com", "apiToken", "", "", "/api")},
16-
{Name: "bar", Context: newContext("http://bar.com", "", "svctok-xxxx", "", "/api/v1")},
17-
{Name: "foobar", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1")},
22+
{Name: "foo", Context: newContext("http://foo.com", "apiToken", "", "", "/api", false, "")},
23+
{Name: "bar", Context: newContext("http://bar.com", "", "svctok-xxxx", "", "/api/v1", false, "")},
24+
{Name: "foobar", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1", false, "")},
25+
{Name: "skipssl", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1", true, "")},
26+
{Name: "privateca", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1", false, privateCaBase64Cert)},
27+
{Name: "selfsigned", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1", false, selfSignedBase64Cert)},
1828
},
1929
}
2030
cli.ConfigPath = filepath.Join(t.TempDir(), "config.yaml")
@@ -25,12 +35,14 @@ func setupConfig(t *testing.T, cli *di.MockDeps) {
2535
}
2636
}
2737

28-
func newContext(url, apiToken, serviceToken, k8sSAToken, apiPath string) *config.StsContext {
38+
func newContext(url, apiToken, serviceToken, k8sSAToken, apiPath string, skipSSL bool, caCertBase64Data string) *config.StsContext {
2939
return &config.StsContext{
30-
URL: url,
31-
APIToken: apiToken,
32-
ServiceToken: serviceToken,
33-
K8sSAToken: k8sSAToken,
34-
APIPath: apiPath,
40+
URL: url,
41+
APIToken: apiToken,
42+
ServiceToken: serviceToken,
43+
K8sSAToken: k8sSAToken,
44+
APIPath: apiPath,
45+
SkipSSL: skipSSL,
46+
CaCertBase64Data: caCertBase64Data,
3547
}
3648
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICJzCCAdGgAwIBAgIUV/aJj1fEcCgNU2FafY0uRLqy7mAwDQYJKoZIhvcNAQEL
3+
BQAwKTEnMCUGA1UEAwwedmlsaWFrb3Yuc2FuZGJveC5zdGFja3N0YXRlLmlvMCAX
4+
DTI1MDcyMTIwNDU1NloYDzIxMjUwNjI3MjA0NTU2WjApMScwJQYDVQQDDB52aWxp
5+
YWtvdi5zYW5kYm94LnN0YWNrc3RhdGUuaW8wXDANBgkqhkiG9w0BAQEFAANLADBI
6+
AkEAopQuOJfIkLCWJKT70hgbHJpUkEB+Za2p9uA1INRKM4Ar7dcV9muxNKOcJZ2s
7+
Vt+b+YSKW8rtmxNPQXuE2D4teQIDAQABo4HOMIHLMB0GA1UdDgQWBBQU0OLVQG12
8+
wMoEKHgqHmZyXSzZ3zAfBgNVHSMEGDAWgBQU0OLVQG12wMoEKHgqHmZyXSzZ3zAP
9+
BgNVHRMBAf8EBTADAQH/MHgGA1UdEQRxMG+CHnZpbGlha292LnNhbmRib3guc3Rh
10+
Y2tzdGF0ZS5pb4Ijb3RscC12aWxpYWtvdi5zYW5kYm94LnN0YWNrc3RhdGUuaW+C
11+
KG90bHAtaHR0cC12aWxpYWtvdi5zYW5kYm94LnN0YWNrc3RhdGUuaW8wDQYJKoZI
12+
hvcNAQELBQADQQAfYAVMeMRGlW+GZkzYOxdHiXX4AHHp9IloZPLmBG4LmvZC80hV
13+
K4cRUEGHkRxgk0h0c9wD8NdVR3QnE0nn6WPE
14+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)