Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implements VerifyBlob functionality #394

Merged
merged 55 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
3a48859
fix: Add contract version to plugin sign request and plugin verify re…
priteshbandi Mar 21, 2024
1caa9bc
bump: bump golang and dependency versions (#392)
Two-Hearts Mar 27, 2024
79ffe18
build(deps): bump actions/stale from 8 to 9 (#391)
dependabot[bot] Apr 2, 2024
fae8a83
Verify blob
priteshbandi Mar 5, 2024
a4feb7d
stash
priteshbandi Mar 21, 2024
2cdf4a2
some more
priteshbandi Apr 2, 2024
8f6a3c9
some more
priteshbandi Apr 2, 2024
560fc53
Merge branch 'main' into BlobVerify
priteshbandi Apr 2, 2024
be64a9e
fix commit
priteshbandi Apr 2, 2024
f274638
fix commit
priteshbandi Apr 2, 2024
0593ee0
fix commit
priteshbandi Apr 2, 2024
9b11321
fix commit
priteshbandi Apr 2, 2024
165867b
fix commit
priteshbandi Apr 2, 2024
e0d0f70
fix commit
priteshbandi Apr 3, 2024
9d1bad5
fix commit
priteshbandi Apr 3, 2024
6619c97
fix commit- increase coverage
priteshbandi Apr 3, 2024
69764d4
fix commit- increase coverage
priteshbandi Apr 3, 2024
bf17936
fix commit- increase coverage
priteshbandi Apr 3, 2024
173bd3b
fix commit- increase coverage
priteshbandi Apr 4, 2024
378fe30
fix commit- increase coverage
priteshbandi Apr 4, 2024
c56fad2
Merge branch 'main' into BlobVerify
priteshbandi Apr 4, 2024
f0fa85f
fix commit- increase coverage
priteshbandi Apr 5, 2024
879cfc7
fix commit- increase coverage
priteshbandi Apr 5, 2024
89b5361
fix commit- increase coverage
priteshbandi Apr 5, 2024
9110836
fix commit- increase coverage
priteshbandi Apr 5, 2024
4fcb165
prefix validatePolicyCore with policy type
priteshbandi Apr 5, 2024
cf9e524
pr feedback
priteshbandi Apr 16, 2024
a81c832
feedback new
priteshbandi May 30, 2024
eed9a81
feedback new1
priteshbandi May 30, 2024
137bd9e
feedback new2
priteshbandi May 30, 2024
37bae40
feedback new3
priteshbandi May 30, 2024
0b46350
feedback new 3
priteshbandi May 30, 2024
6f3a093
Merge branch 'main' into BlobVerify
priteshbandi May 30, 2024
f7688b0
feedback new 4
priteshbandi May 30, 2024
02a0b9e
feedback new 5
priteshbandi May 30, 2024
f24b237
Update verifier/verifier.go
priteshbandi Jun 4, 2024
b72edb3
Update verifier/verifier.go
priteshbandi Jun 4, 2024
c55bcbe
Update verifier/verifier.go
priteshbandi Jun 4, 2024
259225b
Merge branch 'main' into BlobVerify
priteshbandi Jun 10, 2024
342ff55
fix
priteshbandi Jun 10, 2024
735f4e5
Merge branch 'main' into BlobVerify
priteshbandi Jun 12, 2024
645d2b4
Update notation.go
priteshbandi Jun 24, 2024
a294c53
Update notation.go
priteshbandi Jun 24, 2024
0bc9e34
Update verifier/trustpolicy/blob.go
priteshbandi Jun 24, 2024
56bd3a9
Update verifier/trustpolicy/oci.go
priteshbandi Jun 24, 2024
7590d6c
Update verifier/verifier.go
priteshbandi Jun 24, 2024
961bdbd
PR feedback 1
priteshbandi Jun 24, 2024
dd2d5a7
feedback 2
priteshbandi Jun 24, 2024
b47d33f
feedback 3
priteshbandi Jun 24, 2024
0733f7b
feedback 4
priteshbandi Jun 24, 2024
c797468
Update verifier/trustpolicy/blob.go
priteshbandi Jun 27, 2024
123038e
Update verifier/trustpolicy/blob.go
priteshbandi Jun 27, 2024
ceea1d9
Update notation.go
priteshbandi Jun 27, 2024
7653b79
feedback 5
priteshbandi Jun 27, 2024
e201168
feedback
priteshbandi Jun 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions dir/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// package dir implements Notation directory structure.
// Package dir implements Notation directory structure.
// [directory spec]: https://github.com/notaryproject/notation/blob/main/specs/directory.md
//
// Example:
Expand Down Expand Up @@ -57,8 +57,14 @@ const (
PathConfigFile = "config.json"
// PathSigningKeys is the signingkeys file relative path.
PathSigningKeys = "signingkeys.json"
// PathTrustPolicy is the trust policy file relative path.
// PathTrustPolicy is the OCI trust policy file relative path.
// Deprecated: PathTrustPolicy exists for historical compatibility and should not be used.
// To get OCI trust policy path, use PathOCITrustPolicy.
PathTrustPolicy = "trustpolicy.json"
// PathOCITrustPolicy is the OCI trust policy file relative path.
PathOCITrustPolicy = "trustpolicy.oci.json"
// PathBlobTrustPolicy is the Blob trust policy file relative path.
PathBlobTrustPolicy = "trustpolicy.blob.json"
// PathPlugins is the plugins directory relative path.
PathPlugins = "plugins"
// LocalKeysDir is the directory name for local key relative path.
Expand Down
85 changes: 85 additions & 0 deletions example_signBlob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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 notation_test

import (
"context"
"fmt"
"strings"

"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/signer"
)

// ExampleSignBlob demonstrates how to use signer.BlobSign to sign arbitrary data.
func Example_signBlob() {
//exampleSigner implements notation.Signer and notation.BlobSigner. Given key and X509 certificate chain,
// it provides method to sign OCI artifacts or blobs.
// Users should replace `exampleCertTuple.PrivateKey` with their own private
// key and replace `exampleCerts` with the corresponding certificate chain,
//following the Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
if err != nil {
panic(err) // Handle error
}

// Both COSE ("application/cose") and JWS ("application/jose+json")
// signature mediaTypes are supported.
exampleSignatureMediaType := jws.MediaTypeEnvelope
exampleContentMediaType := "video/mp4"

// exampleSignOptions is an example of notation.SignBlobOptions.
exampleSignOptions := notation.SignBlobOptions{
SignerSignOptions: notation.SignerSignOptions{
SignatureMediaType: exampleSignatureMediaType,
SigningAgent: "example signing agent",
},
ContentMediaType: exampleContentMediaType,
UserMetadata: map[string]string{"buildId": "101"},
}

// exampleReader reads the data that needs to be signed. This data can be in a file or in memory.
exampleReader := strings.NewReader("example blob")

// Upon successful signing, signature envelope and signerInfo are returned.
// signatureEnvelope can be used in a verification process later on.
signatureEnvelope, signerInfo, err := notation.SignBlob(context.Background(), exampleSigner, exampleReader, exampleSignOptions)
if err != nil {
panic(err) // Handle error
}

fmt.Println("Successfully signed")

// a peek of the signature envelope generated
sigBlob, err := signature.ParseEnvelope(exampleSignatureMediaType, signatureEnvelope)
if err != nil {
panic(err) // Handle error
}
sigContent, err := sigBlob.Content()
if err != nil {
panic(err) // Handle error
}
fmt.Println("signature Payload ContentType:", sigContent.Payload.ContentType)
fmt.Println("signature Payload Content:", string(sigContent.Payload.Content))
fmt.Println("signerInfo SigningAgent:", signerInfo.UnsignedAttributes.SigningAgent)

// Output:
// Successfully signed
// signature Payload ContentType: application/vnd.cncf.notary.payload.v1+json
// signature Payload Content: {"targetArtifact":{"annotations":{"buildId":"101"},"digest":"sha384:b8ab24dafba5cf7e4c89c562f811cf10493d4203da982d3b1345f366ca863d9c2ed323dbd0fb7ff83a80302ceffa5a61","mediaType":"video/mp4","size":12}}
// signerInfo SigningAgent: example signing agent
}
151 changes: 151 additions & 0 deletions example_verifyBlob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// 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 notation_test

import (
"context"
"fmt"
"os"
"strings"

"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/verifier"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
)

// examplePolicyDocument is an example of a valid trust policy document.
// trust policy document should follow this spec:
// https://github.com/notaryproject/notaryproject/blob/v1.1.0/specs/trust-store-trust-policy.md#trust-policy
var exampleBlobPolicyDocument = trustpolicy.BlobDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.BlobTrustPolicy{
{
Name: "test-statement-name",
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name, Override: map[trustpolicy.ValidationType]trustpolicy.ValidationAction{trustpolicy.TypeRevocation: trustpolicy.ActionSkip}},
TrustStores: []string{"ca:valid-trust-store"},
TrustedIdentities: []string{"*"},
},
},
}

// ExampleVerifyBlob demonstrates how to use verifier.Verify to verify a
// signature of the blob.
func Example_verifyBlob() {
// Both COSE ("application/cose") and JWS ("application/jose+json")
// signature mediaTypes are supported.
exampleSignatureMediaType := jws.MediaTypeEnvelope

// exampleSignatureEnvelope is a valid signature envelope.
exampleSignatureEnvelope := getSignatureEnvelope()

// createTrustStoreForBlobVerify creates a trust store directory for demo purpose.
// Users could use the default trust store from Notary and add trusted
// certificates into it following the trust store spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
if err := createTrustStoreForBlobVerify(); err != nil {
panic(err) // Handle error
}

// exampleVerifier implements notation.Verify and notation.VerifyBlob.
exampleVerifier, err := verifier.NewVerifier(nil, &exampleBlobPolicyDocument, truststore.NewX509TrustStore(dir.ConfigFS()), nil)
if err != nil {
panic(err) // Handle error
}

// exampleReader reads the data that needs to be verified. This data can be in a file or in memory.
exampleReader := strings.NewReader("example blob")

// exampleVerifyOptions is an example of notation.VerifierVerifyOptions
exampleVerifyOptions := notation.VerifyBlobOptions{
BlobVerifierVerifyOptions: notation.BlobVerifierVerifyOptions{
SignatureMediaType: exampleSignatureMediaType,
TrustPolicyName: "test-statement-name",
},
}

// upon successful verification, the signature verification outcome is
// returned.
_, outcome, err := notation.VerifyBlob(context.Background(), exampleVerifier, exampleReader, []byte(exampleSignatureEnvelope), exampleVerifyOptions)
if err != nil {
panic(err) // Handle error
}

fmt.Println("Successfully verified")

// a peek of the payload inside the signature envelope
fmt.Println("payload ContentType:", outcome.EnvelopeContent.Payload.ContentType)

// Note, upon successful verification, payload.TargetArtifact from the
// signature envelope matches exactly with our exampleTargetDescriptor.
// (This check has been done for the user inside verifier.Verify.)
fmt.Println("payload Content:", string(outcome.EnvelopeContent.Payload.Content))

// Output:
// Successfully verified
// payload ContentType: application/vnd.cncf.notary.payload.v1+json
// payload Content: {"targetArtifact":{"digest":"sha384:b8ab24dafba5cf7e4c89c562f811cf10493d4203da982d3b1345f366ca863d9c2ed323dbd0fb7ff83a80302ceffa5a61","mediaType":"video/mp4","size":12}}
}

func createTrustStoreForBlobVerify() error {
// changing the path of the trust store for demo purpose.
// Users could keep the default value, i.e. os.UserConfigDir.
dir.UserConfigDir = "tmp"

// an example of a valid X509 self-signed certificate for demo purpose ONLY.
// (This self-signed cert is paired with the private key used to
// generate the `exampleSignatureEnvelopePem` above.)
// Users should replace `exampleX509Certificate` with their own trusted
// certificate and add to the trust store, following the
// Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
MIIEbDCCAtSgAwIBAgIBUzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzEL
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEl
MCMGA1UEAxMcTm90YXRpb24gRXhhbXBsZSBzZWxmLXNpZ25lZDAgFw0yNDA0MDQy
MTIwMjBaGA8yMTI0MDQwNDIxMjAyMFowZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
AldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxJTAjBgNVBAMT
HE5vdGF0aW9uIEV4YW1wbGUgc2VsZi1zaWduZWQwggGiMA0GCSqGSIb3DQEBAQUA
A4IBjwAwggGKAoIBgQDGIiN4yCjSVqFELZwxK/BMb8BokP587L8oPrZ1g8H7LudB
moLNDT7vF9xccbCfU3yNuOd0WaOgnENiCs81VHidyJsj1Oz3u+0Zn3ng7V+uZr6m
AIO74efA9ClMiY4i4HIt8IAZF57AL2mzDnCITgSWxikf030Il85MI42STvA+qYuz
ZEOp3XvKo8bDgQFvbtgK0HYYMfrka7VDmIWVo0rBMGm5btI8HOYQ0r9aqsrCxLAv
1AQeOQm+wbRcp4R5PIUJr+REGn7JCbOyXg/7qqHXKKmvV5yrGaraw8gZ5pqP/RHK
XUJIfvD0Vf2epJmsvC+6vXkSWtz+cA8J4GQx4J4SXL57hoYkC5qv39SOLzlWls3I
6fgeO+SZ0sceMd8NKlom/L5eOJBfB3bTQB83hq/3bRtjT7/qCMsL3VcndKkS+vGF
JPw5uTH+pmBgHrLr6tRoRRjwRFuZ0dO05AbdjCaxgVDtFI3wNbaXn/1VlRGySQIS
UNWxCrUsSzndeqwmjqsCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM
MAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBgQBdi0SaJAaeKBB0I+Fjcbmc
4zRvHE4GDSMSDnAK97nrZCZ9iwKuY4x6mv9lwQe2P3VXROoL9JmONNf0yaObOwQj
ILGnbe2rzYtUardz2gzh+6KNzJHspRvk1f06mp4496XQ3STMRSr8kno1svKQMy0Y
FRsGMKs4fWHavIAqNXg9ymrZvvXiatN2UiVtAA/jBFScZAWskeb2WHNzORi7H5Z1
mp5+IlNYQpzdIu/dvLVxzhh2UvkRdsQqsMgt/MOU84RncwUNZM4yI5EGPoaSJdsj
AGNd+UV6ur7QmVI2Q9EZNRlaDJtaoZmKns5j1SlmDXWKbdRmw42ORDudODj/pHA9
+u+ca9t3uLsbqO9yPm8m+6fyxffWS11QAH6O7EjydJWcEe5tYkPpL6kcaEyQKESm
5CDlsk+W3ElpaUu6tsnGKODvgdAN3m0noC+qxzCMqoCM4+M5V6OptR98MDl2FK0B
5+WF6YHBxf/uqDvFktUczjrIWuyfECywp05bpGAErGE=
-----END CERTIFICATE-----`

// Adding the certificate into the trust store.
if err := os.MkdirAll("tmp/truststore/x509/ca/valid-trust-store", 0700); err != nil {
return err
}
return os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationBlobExample.pem", []byte(exampleX509Certificate), 0600)
}

func getSignatureEnvelope() string {
return `{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEzODQ6YjhhYjI0ZGFmYmE1Y2Y3ZTRjODljNTYyZjgxMWNmMTA0OTNkNDIwM2RhOTgyZDNiMTM0NWYzNjZjYTg2M2Q5YzJlZDMyM2RiZDBmYjdmZjgzYTgwMzAyY2VmZmE1YTYxIiwibWVkaWFUeXBlIjoidmlkZW8vbXA0Iiwic2l6ZSI6MTJ9fQ","protected":"eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDI0LTA0LTA0VDE0OjIwOjIxLTA3OjAwIn0","header":{"x5c":["MIIEbDCCAtSgAwIBAgIBUzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTElMCMGA1UEAxMcTm90YXRpb24gRXhhbXBsZSBzZWxmLXNpZ25lZDAgFw0yNDA0MDQyMTIwMjBaGA8yMTI0MDQwNDIxMjAyMFowZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxJTAjBgNVBAMTHE5vdGF0aW9uIEV4YW1wbGUgc2VsZi1zaWduZWQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDGIiN4yCjSVqFELZwxK/BMb8BokP587L8oPrZ1g8H7LudBmoLNDT7vF9xccbCfU3yNuOd0WaOgnENiCs81VHidyJsj1Oz3u+0Zn3ng7V+uZr6mAIO74efA9ClMiY4i4HIt8IAZF57AL2mzDnCITgSWxikf030Il85MI42STvA+qYuzZEOp3XvKo8bDgQFvbtgK0HYYMfrka7VDmIWVo0rBMGm5btI8HOYQ0r9aqsrCxLAv1AQeOQm+wbRcp4R5PIUJr+REGn7JCbOyXg/7qqHXKKmvV5yrGaraw8gZ5pqP/RHKXUJIfvD0Vf2epJmsvC+6vXkSWtz+cA8J4GQx4J4SXL57hoYkC5qv39SOLzlWls3I6fgeO+SZ0sceMd8NKlom/L5eOJBfB3bTQB83hq/3bRtjT7/qCMsL3VcndKkS+vGFJPw5uTH+pmBgHrLr6tRoRRjwRFuZ0dO05AbdjCaxgVDtFI3wNbaXn/1VlRGySQISUNWxCrUsSzndeqwmjqsCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBgQBdi0SaJAaeKBB0I+Fjcbmc4zRvHE4GDSMSDnAK97nrZCZ9iwKuY4x6mv9lwQe2P3VXROoL9JmONNf0yaObOwQjILGnbe2rzYtUardz2gzh+6KNzJHspRvk1f06mp4496XQ3STMRSr8kno1svKQMy0YFRsGMKs4fWHavIAqNXg9ymrZvvXiatN2UiVtAA/jBFScZAWskeb2WHNzORi7H5Z1mp5+IlNYQpzdIu/dvLVxzhh2UvkRdsQqsMgt/MOU84RncwUNZM4yI5EGPoaSJdsjAGNd+UV6ur7QmVI2Q9EZNRlaDJtaoZmKns5j1SlmDXWKbdRmw42ORDudODj/pHA9+u+ca9t3uLsbqO9yPm8m+6fyxffWS11QAH6O7EjydJWcEe5tYkPpL6kcaEyQKESm5CDlsk+W3ElpaUu6tsnGKODvgdAN3m0noC+qxzCMqoCM4+M5V6OptR98MDl2FK0B5+WF6YHBxf/uqDvFktUczjrIWuyfECywp05bpGAErGE="],"io.cncf.notary.signingAgent":"example signing agent"},"signature":"liOjdgQ9BKuQTZGXRh3o6P8AMUIq_MKQReEcqA5h8M4RYs3DV_wXfaLCr2x_NRcwjTZsoO1_J77hmzkkk4L0IuFP8Qw0KKtmc83G0yFi4yYV5fwzrIbnhC2GRLuqLPnK-C4qYmv52ld3ebvo7XWwRHu30-VXePmTRFp6iG-eSAgkNgwhxSZ0ZmTFLG3ceNiX2bxpLHlXdPwA3aFKbd6nKrzo4CZ1ZyLNmAIaoA5-kmc0Hyt45trpxaaiWusI_pcTLw71YCqEAs32tEq3q6hRAgAZZN-Qvm9GyNp9EuaPiKjMbJFqtjome5ITxyNd-5t09dDCUgSe3t-iqv2Blm4E080AP1TYwUKLYklGniUP1dAtOau5G2juZLpl7tr4LQ99mycflnAmV7e79eEWXffvy5EAl77dW4_vM7lEemm08m2wddGuDOWXYb1j1r2_a5Xb92umHq6ZMhAp200A0pUkm9640x8z5jdudi_7KeezdqUK7ZMmSxHohiylyKD_20Cy"}`
}
Loading
Loading