From b96330dd2efa3d98c61b89f10aa4097b84b9c1ff Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 6 Jan 2025 09:11:32 +0000 Subject: [PATCH] test: added 2 E2E test cases Signed-off-by: Junjie Gao --- cmd/notation/blob/inspect.go | 6 +- cmd/notation/inspect.go | 10 +- internal/envelope/signature.go | 87 ++++++------- internal/envelope/signature_test.go | 2 +- test/e2e/run.sh | 2 +- test/e2e/suite/command/blob/blob_test.go | 26 ++++ test/e2e/suite/command/blob/inspect.go | 116 ++++++++++++++++++ .../config/signatures/LICENSE.jws.sig | 1 + 8 files changed, 197 insertions(+), 53 deletions(-) create mode 100644 test/e2e/suite/command/blob/blob_test.go create mode 100644 test/e2e/suite/command/blob/inspect.go create mode 100644 test/e2e/testdata/config/signatures/LICENSE.jws.sig diff --git a/cmd/notation/blob/inspect.go b/cmd/notation/blob/inspect.go index 60bf0dac0..652890c07 100644 --- a/cmd/notation/blob/inspect.go +++ b/cmd/notation/blob/inspect.go @@ -67,12 +67,12 @@ func runInspect(opts *inspectOpts) error { return err } - sigBlob, err := os.ReadFile(opts.sigPath) + envelopeBytes, err := os.ReadFile(opts.sigPath) if err != nil { return err } - sig, err := envelope.Parse(sigBlob, envelopeMediaType) + sig, err := envelope.Parse(envelopeMediaType, envelopeBytes) if err != nil { return err } @@ -84,7 +84,7 @@ func runInspect(opts *inspectOpts) error { case cmd.OutputJSON: return ioutil.PrintObjectAsJSON(sig) case cmd.OutputPlaintext: - sig.SignatureNode(opts.sigPath).Print() + sig.ToNode(opts.sigPath).Print() } return nil } diff --git a/cmd/notation/inspect.go b/cmd/notation/inspect.go index 79bca07e9..f1ddb985e 100644 --- a/cmd/notation/inspect.go +++ b/cmd/notation/inspect.go @@ -39,8 +39,8 @@ type inspectOpts struct { } type inspectOutput struct { - MediaType string `json:"mediaType"` - Signatures []*envelope.SignatureInfo `json:"signatures"` + MediaType string `json:"mediaType"` + Signatures []*envelope.Signature `json:"signatures"` } func inspectCommand(opts *inspectOpts) *cobra.Command { @@ -111,7 +111,7 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { if err != nil { return err } - output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []*envelope.SignatureInfo{}} + output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []*envelope.Signature{}} skippedSignatures := false err = listSignatures(ctx, sigRepo, manifestDesc, opts.maxSignatures, func(sigManifestDesc ocispec.Descriptor) error { sigBlob, sigDesc, err := sigRepo.FetchSignatureBlob(ctx, sigManifestDesc) @@ -121,7 +121,7 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { return nil } - sig, err := envelope.Parse(sigBlob, sigDesc.MediaType) + sig, err := envelope.Parse(sigDesc.MediaType, sigBlob) if err != nil { logSkippedSignature(sigManifestDesc, err) skippedSignatures = true @@ -178,7 +178,7 @@ func printOutput(outputFormat string, ref string, output inspectOutput) error { cncfSigNode := root.Add(registry.ArtifactTypeNotation) for _, signature := range output.Signatures { - cncfSigNode.Children = append(cncfSigNode.Children, signature.SignatureNode(signature.Digest)) + cncfSigNode.Children = append(cncfSigNode.Children, signature.ToNode(signature.Digest)) } root.Print() diff --git a/internal/envelope/signature.go b/internal/envelope/signature.go index fd4bfaeb3..2bb3028c4 100644 --- a/internal/envelope/signature.go +++ b/internal/envelope/signature.go @@ -31,33 +31,41 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -// SignatureInfo is the signature envelope with human readable fields. -type SignatureInfo struct { +// Signature is the signature envelope for printing in human readable format. +type Signature struct { MediaType string `json:"mediaType"` Digest string `json:"digest,omitempty"` SignatureAlgorithm plugin.SignatureAlgorithm `json:"signatureAlgorithm"` SignedAttributes map[string]any `json:"signedAttributes"` UserDefinedAttributes map[string]string `json:"userDefinedAttributes"` UnsignedAttributes map[string]any `json:"unsignedAttributes"` - Certificates []CertificateInfo `json:"certificates"` + Certificates []Certificate `json:"certificates"` SignedArtifact ocispec.Descriptor `json:"signedArtifact"` } -type CertificateInfo struct { +// Certificate is the certificate information for printing in human readable +// format. +type Certificate struct { SHA256Fingerprint string `json:"SHA256Fingerprint"` IssuedTo string `json:"issuedTo"` IssuedBy string `json:"issuedBy"` Expiry ioutil.Time `json:"expiry"` } -type TimestampInfo struct { - Timestamp ioutil.Timestamp `json:"timestamp,omitempty"` - Certificates []CertificateInfo `json:"certificates,omitempty"` - Error string `json:"error,omitempty"` +// Timestamp is the timestamp information for printing in human readable. +type Timestamp struct { + Timestamp ioutil.Timestamp `json:"timestamp,omitempty"` + Certificates []Certificate `json:"certificates,omitempty"` + Error string `json:"error,omitempty"` } -func Parse(sig []byte, envelopeMediaType string) (*SignatureInfo, error) { - sigEnvelope, err := signature.ParseEnvelope(envelopeMediaType, sig) +// Parse parses the signature blob and returns a Signature object. +// +// envelopeMediaType supports +// - application/jose+json +// - application/cose +func Parse(envelopeMediaType string, envelopeBytes []byte) (*Signature, error) { + sigEnvelope, err := signature.ParseEnvelope(envelopeMediaType, envelopeBytes) if err != nil { return nil, err } @@ -76,8 +84,7 @@ func Parse(sig []byte, envelopeMediaType string) (*SignatureInfo, error) { if err != nil { return nil, err } - - return &SignatureInfo{ + return &Signature{ MediaType: envelopeMediaType, SignatureAlgorithm: signatureAlgorithm, SignedAttributes: getSignedAttributes(envelopeContent), @@ -93,15 +100,13 @@ func getSignedAttributes(envContent *signature.EnvelopeContent) map[string]any { "signingScheme": envContent.SignerInfo.SignedAttributes.SigningScheme, "signingTime": ioutil.Time(envContent.SignerInfo.SignedAttributes.SigningTime), } - expiry := envContent.SignerInfo.SignedAttributes.Expiry - if !expiry.IsZero() { + if expiry := envContent.SignerInfo.SignedAttributes.Expiry; !expiry.IsZero() { signedAttributes["expiry"] = ioutil.Time(expiry) } for _, attribute := range envContent.SignerInfo.SignedAttributes.ExtendedAttributes { signedAttributes[fmt.Sprint(attribute.Key)] = fmt.Sprint(attribute.Value) } - return signedAttributes } @@ -115,57 +120,52 @@ func getUnsignedAttributes(envContent *signature.EnvelopeContent) map[string]any if envContent.SignerInfo.UnsignedAttributes.SigningAgent != "" { unsignedAttributes["signingAgent"] = envContent.SignerInfo.UnsignedAttributes.SigningAgent } - return unsignedAttributes } -func getCertificates(certChain []*x509.Certificate) []CertificateInfo { - certificates := []CertificateInfo{} +func getCertificates(certChain []*x509.Certificate) []Certificate { + certificates := []Certificate{} for _, cert := range certChain { - h := sha256.Sum256(cert.Raw) - fingerprint := strings.ToLower(hex.EncodeToString(h[:])) + hash := sha256.Sum256(cert.Raw) - certificate := CertificateInfo{ - SHA256Fingerprint: fingerprint, + certificates = append(certificates, Certificate{ + SHA256Fingerprint: strings.ToLower(hex.EncodeToString(hash[:])), IssuedTo: cert.Subject.String(), IssuedBy: cert.Issuer.String(), Expiry: ioutil.Time(cert.NotAfter), - } - - certificates = append(certificates, certificate) + }) } - return certificates } -func parseTimestamp(signerInfo signature.SignerInfo) TimestampInfo { +func parseTimestamp(signerInfo signature.SignerInfo) Timestamp { signedToken, err := tspclient.ParseSignedToken(signerInfo.UnsignedAttributes.TimestampSignature) if err != nil { - return TimestampInfo{ - Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()), + return Timestamp{ + Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err), } } info, err := signedToken.Info() if err != nil { - return TimestampInfo{ - Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()), + return Timestamp{ + Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err), } } timestamp, err := info.Validate(signerInfo.Signature) if err != nil { - return TimestampInfo{ - Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()), + return Timestamp{ + Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err), } } - return TimestampInfo{ + return Timestamp{ Timestamp: ioutil.Timestamp(*timestamp), Certificates: getCertificates(signedToken.Certificates), } } -// SignatureNode returns a tree node that represents the signature. -func (s *SignatureInfo) SignatureNode(sigName string) *tree.Node { +// ToNode returns a tree node that represents the signature. +func (s *Signature) ToNode(sigName string) *tree.Node { sigNode := tree.New(sigName) sigNode.AddPair("signature algorithm", s.SignatureAlgorithm) sigNode.AddPair("signature envelope type", s.MediaType) @@ -181,7 +181,7 @@ func (s *SignatureInfo) SignatureNode(sigName string) *tree.Node { switch value := v.(type) { case string: unsignedAttributesNode.AddPair(k, value) - case TimestampInfo: + case Timestamp: timestampNode := unsignedAttributesNode.Add("timestamp signature") if value.Error != "" { timestampNode.AddPair("error", value.Error) @@ -202,16 +202,17 @@ func (s *SignatureInfo) SignatureNode(sigName string) *tree.Node { } func addMapToTree[T any](node *tree.Node, m map[string]T) { - if len(m) > 0 { - for k, v := range m { - node.AddPair(k, v) - } - } else { + if len(m) == 0 { node.Add("(empty)") + return + } + + for k, v := range m { + node.AddPair(k, v) } } -func addCertificatesToTree(node *tree.Node, name string, certs []CertificateInfo) { +func addCertificatesToTree(node *tree.Node, name string, certs []Certificate) { certListNode := node.Add(name) for _, cert := range certs { certNode := certListNode.AddPair("SHA256 fingerprint", cert.SHA256Fingerprint) diff --git a/internal/envelope/signature_test.go b/internal/envelope/signature_test.go index 8ef6f4e8f..6a469dc05 100644 --- a/internal/envelope/signature_test.go +++ b/internal/envelope/signature_test.go @@ -29,7 +29,7 @@ func TestGetUnsignedAttributes(t *testing.T) { } expectedErrMsg := "failed to parse timestamp countersignature: cms: syntax error: invalid signed data: failed to convert from BER to DER: asn1: syntax error: decoding BER length octets: short form length octets value should be less or equal to the subsequent octets length" unsignedAttr := getUnsignedAttributes(envContent) - val, ok := unsignedAttr["timestampSignature"].(TimestampInfo) + val, ok := unsignedAttr["timestampSignature"].(Timestamp) if !ok { t.Fatal("expected to have timestampSignature") } diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 4a8e28f05..a3db99835 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -118,4 +118,4 @@ export NOTATION_E2E_PLUGIN_TAR_GZ_PATH=$CWD/plugin/bin/$PLUGIN_NAME.tar.gz export NOTATION_E2E_MALICIOUS_PLUGIN_ARCHIVE_PATH=$CWD/testdata/malicious-plugin # run tests -ginkgo -r -p -v \ No newline at end of file +ginkgo -r -p -v --focus "notation blob inspect" \ No newline at end of file diff --git a/test/e2e/suite/command/blob/blob_test.go b/test/e2e/suite/command/blob/blob_test.go new file mode 100644 index 000000000..b500e346f --- /dev/null +++ b/test/e2e/suite/command/blob/blob_test.go @@ -0,0 +1,26 @@ +// Copyright The Notary Project Authors. +// 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 blob + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCommand(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Blob Command Suite") +} diff --git a/test/e2e/suite/command/blob/inspect.go b/test/e2e/suite/command/blob/inspect.go new file mode 100644 index 000000000..f4939fc08 --- /dev/null +++ b/test/e2e/suite/command/blob/inspect.go @@ -0,0 +1,116 @@ +// Copyright The Notary Project Authors. +// 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 blob + +import ( + "path/filepath" + + . "github.com/notaryproject/notation/test/e2e/internal/notation" + "github.com/notaryproject/notation/test/e2e/internal/utils" + . "github.com/onsi/ginkgo/v2" +) + +const ( + jwsBlobSig = "LICENSE.jws.sig" +) + +var _ = Describe("notation blob inspect", func() { + It("with timestamping", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + expectedKeyWords := `├── signature algorithm: RSASSA-PSS-SHA-256 +├── signature envelope type: application/jose+json +├── signed attributes +│ ├── signingScheme: notary.x509 +│ └── signingTime: Tue Dec 31 08:05:29 2024 +├── user defined attributes +│ └── (empty) +├── unsigned attributes +│ ├── timestamp signature +│ │ ├── timestamp: [Tue Dec 31 08:05:29 2024, Tue Dec 31 08:05:30 2024] +│ │ └── certificates +│ │ ├── SHA256 fingerprint: 36e731cfa9bfd69dafb643809f6dec500902f7197daeaad86ea0159a2268a2b8 +│ │ │ ├── issued to: CN=Microsoft Public RSA Timestamping CA 2020,O=Microsoft Corporation,C=US +│ │ │ ├── issued by: CN=Microsoft Identity Verification Root Certificate Authority 2020,O=Microsoft Corporation,C=US +│ │ │ └── expiry: Mon Nov 19 20:42:31 2035 +│ │ └── SHA256 fingerprint: 93db2732c49e2624cf0a5cc03ad04acc0927fcaf5e7afdd4a3e23b6fc196aedc +│ │ ├── issued to: CN=Microsoft Public RSA Time Stamping Authority,OU=Microsoft America Operations+OU=nShield TSS ESN:7800-05E0-D947,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US +│ │ ├── issued by: CN=Microsoft Public RSA Timestamping CA 2020,O=Microsoft Corporation,C=US +│ │ └── expiry: Sat Feb 15 20:36:12 2025 +│ └── signingAgent: notation-go/1.3.0+unreleased +├── certificates +│ └── SHA256 fingerprint: dadee19c843e94b94daae9854d0de7ad93642b6075e2d1523b860b1770b64a03 +│ ├── issued to: CN=testcert2,O=Notary,L=Seattle,ST=WA,C=US +│ ├── issued by: CN=testcert2,O=Notary,L=Seattle,ST=WA,C=US +│ └── expiry: Wed Jan 1 08:04:39 2025 +└── signed artifact + ├── media type: application/octet-stream + ├── digest: sha256:c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 + └── size: 11357 +` + notation.Exec("blob", "inspect", filepath.Join(NotationE2EConfigPath, "signatures", jwsBlobSig)). + MatchKeyWords(expectedKeyWords) + }) + }) + + It("with timestamping and output as json", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + expectedContent := `{ + "mediaType": "application/jose+json", + "signatureAlgorithm": "RSASSA-PSS-SHA-256", + "signedAttributes": { + "signingScheme": "notary.x509", + "signingTime": "2024-12-31T08:05:29Z" + }, + "userDefinedAttributes": null, + "unsignedAttributes": { + "signingAgent": "notation-go/1.3.0+unreleased", + "timestampSignature": { + "timestamp": "[2024-12-31T08:05:29Z, 2024-12-31T08:05:30Z]", + "certificates": [ + { + "SHA256Fingerprint": "36e731cfa9bfd69dafb643809f6dec500902f7197daeaad86ea0159a2268a2b8", + "issuedTo": "CN=Microsoft Public RSA Timestamping CA 2020,O=Microsoft Corporation,C=US", + "issuedBy": "CN=Microsoft Identity Verification Root Certificate Authority 2020,O=Microsoft Corporation,C=US", + "expiry": "2035-11-19T20:42:31Z" + }, + { + "SHA256Fingerprint": "93db2732c49e2624cf0a5cc03ad04acc0927fcaf5e7afdd4a3e23b6fc196aedc", + "issuedTo": "CN=Microsoft Public RSA Time Stamping Authority,OU=Microsoft America Operations+OU=nShield TSS ESN:7800-05E0-D947,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US", + "issuedBy": "CN=Microsoft Public RSA Timestamping CA 2020,O=Microsoft Corporation,C=US", + "expiry": "2025-02-15T20:36:12Z" + } + ] + } + }, + "certificates": [ + { + "SHA256Fingerprint": "dadee19c843e94b94daae9854d0de7ad93642b6075e2d1523b860b1770b64a03", + "issuedTo": "CN=testcert2,O=Notary,L=Seattle,ST=WA,C=US", + "issuedBy": "CN=testcert2,O=Notary,L=Seattle,ST=WA,C=US", + "expiry": "2025-01-01T08:04:39Z" + } + ], + "signedArtifact": { + "mediaType": "application/octet-stream", + "digest": "sha256:c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", + "size": 11357 + } +} +` + notation.Exec("blob", "inspect", "--output", "json", filepath.Join(NotationE2EConfigPath, "signatures", jwsBlobSig)). + MatchContent(expectedContent) + }) + }) + +}) diff --git a/test/e2e/testdata/config/signatures/LICENSE.jws.sig b/test/e2e/testdata/config/signatures/LICENSE.jws.sig new file mode 100644 index 000000000..1aaab8461 --- /dev/null +++ b/test/e2e/testdata/config/signatures/LICENSE.jws.sig @@ -0,0 +1 @@ +{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6YzcxZDIzOWRmOTE3MjZmYzUxOWM2ZWI3MmQzMThlYzY1ODIwNjI3MjMyYjJmNzk2MjE5ZTg3ZGNmMzVkMGFiNCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL29jdGV0LXN0cmVhbSIsInNpemUiOjExMzU3fX0","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDI0LTEyLTMxVDA4OjA1OjI5WiJ9","header":{"io.cncf.notary.timestampSignature":"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","x5c":["MIIDRTCCAi2gAwIBAgICAIAwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxEjAQBgNVBAMTCXRlc3RjZXJ0MjAeFw0yNDEyMzEwODA0MzlaFw0yNTAxMDEwODA0MzlaMFExCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJXQTEQMA4GA1UEBxMHU2VhdHRsZTEPMA0GA1UEChMGTm90YXJ5MRIwEAYDVQQDEwl0ZXN0Y2VydDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDi+tSWfdTY4r5DNqw7kegX9wMpbvmo9FR9nUJLEvWQmWxNCSjNXzrcDlpaYloHLw6Ql9WkeO5GbIsTndkthZIiVNGvAmTWEy/UNBQdFTfXR5XVfqBRTw/pCdv0oG7W/a+G3GdMMDRxx0iOeLkFhK5wBN3h4Cf16I3JUKjHoYAGfsCqFUUSFvMvaIEXVNP9CBSPidVL5GkGFV+vUJPRW9lSUvCf0mqYZDBvtGN+3t2apAxX18l0dxRCPuRBhlJMVDGKJqmmhzVYi0EaSG0BzcXssdb41i1/kpjRx4Gin/C1+qHWegvpc4CE9bAuEq1aVUusRXN5NjEFtzNNKsQdnfo1AgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAihI3BqLTExpk+VgM7/Rupe96YxeG/ZNZFkDgacSBvfry4l2SUQSLbg3aI19Gvof2SkO2zC56MK3qyoVvFhIqfURYpZzIs00GMFFqu4pWaqz7cJMBsucQrAr6/HvJAlPOn7AbSbV47DOvUmpJ074ozAQQS7hGO1EwNbGMI73VAwGBSyprxWqHnqPNfT20C6iiXXkWXXEQhsl+Lui8TchF8VdP+Gmnb73TpkX6EzI7Ri2tbrmebZxR5LMddwNRH8i9hiTua5lSMXFP3/ev547T4AoSh5/3WDfrONKikYXKhdEnS+OgLl9wL5NHMFzAEeKt3hCCbuT4ZXskoLURD0GckQ=="],"io.cncf.notary.signingAgent":"notation-go/1.3.0+unreleased"},"signature":"WHDX5zUkAepknd1xKlGfN1aBvJ4aF1PNNm9x_DBN_ULbZQ5JEJVJfSo4sjAuYRJ5ht35p43x8TRWJmBlUFreh3mg_-TRsSovqJ5ousqmIIlTjv9naQeJ78IsfXM8eczOTc84wgsqLZ-7rabof2C9VpsOc3mTr-IdfcIliKknh5nDpXV79NaVpQuJg7n0VrK3gTX9IFid9WlqkKznojLN-dDRl3Jk_FbHtWteCpzY0pAgcMSlnVkN_FIKkiyv5VbjqdQc1JhHwwyD-3ECBD2kUKeCZv9Tz8PzAEtySNQ5qq-bVBM6PIEFZn2guTjKhCE6sfnueWnWoxpd4IkU-8agPQ"} \ No newline at end of file