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

Add TrustRoot crd. #291

Merged
merged 3 commits into from
Dec 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 97 additions & 0 deletions .github/workflows/kind-e2e-trustroot-crd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright 2022 The Sigstore 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.

name: TrustRoot CRD KinD E2E

on:
pull_request:
branches: [ 'main', 'release-*' ]

permissions: read-all

jobs:
e2e-crd-tests:
name: e2e CRD tests
runs-on: ubuntu-latest

strategy:
fail-fast: false # Keep running if one leg fails.
matrix:
k8s-version:
- v1.22.x
- v1.23.x
- v1.24.x
- v1.25.x

env:
# https://github.com/google/go-containerregistry/pull/125 allows insecure registry for
# '*.local' hostnames.
REGISTRY_NAME: registry.local
REGISTRY_PORT: 5000
INSECURE_REGISTRY_NAME: insecure-registry.notlocal
INSECURE_REGISTRY_PORT: 5001
KO_DOCKER_REPO: registry.local:5000/policy-controller

steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v2.4.0
- uses: actions/setup-go@d0a58c1c4d2b25278816e339b944508c875f3613 # v2.2.0
with:
go-version: '1.19'
check-latest: true

- uses: imjasonh/setup-ko@ace48d793556083a76f1e3e6068850c1f4a369aa # v0.6

- uses: imranismail/setup-kustomize@a76db1c6419124d51470b1e388c4b29476f495f1 # v1.6.1

- name: Install yq
uses: mikefarah/yq@42e7c3cdf08ccd664875d7ac685c7414b61962be # v4.27.3

- uses: sigstore/cosign-installer@main
with:
cosign-release: 'v1.13.1'

- name: Setup mirror
uses: chainguard-dev/actions/setup-mirror@main
with:
mirror: mirror.gcr.io

- name: Setup kind cluster
uses: chainguard-dev/actions/setup-kind@main
with:
k8s-version: ${{ matrix.k8s-version }}
cluster-suffix: c${{ github.run_id }}.local

- name: Install policy-controller
env:
GIT_HASH: ${{ github.sha }}
GIT_VERSION: ci
LDFLAGS: ""
POLICY_CONTROLLER_YAML: policy-controller-e2e.yaml
KO_PREFIX: registry.local:5000/policy-controller
POLICY_CONTROLLER_ARCHS: linux/amd64
run: |
make ko-policy-controller
kubectl apply -f policy-controller-e2e.yaml

# Wait for the webhook to come up and become Ready
kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook
kubectl wait deployment -n cosign-system --for condition=Available=True --timeout=90s --all

- name: Run TrustRoot CRD e2e tests
run: |
./test/e2e_test_trustroot_crd.sh

- name: Collect diagnostics
if: ${{ failure() }}
uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main
3 changes: 3 additions & 0 deletions cmd/policy_webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/sigstore/policy-controller/pkg/apis/policy/v1beta1"
"github.com/sigstore/policy-controller/pkg/config"
"github.com/sigstore/policy-controller/pkg/reconciler/clusterimagepolicy"
"github.com/sigstore/policy-controller/pkg/reconciler/trustroot"

// Register the provider-specific plugins
_ "github.com/sigstore/sigstore/pkg/signature/kms/aws"
Expand Down Expand Up @@ -74,6 +75,7 @@ var (
var types = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{
// v1alpha1
v1alpha1.SchemeGroupVersion.WithKind("ClusterImagePolicy"): &v1alpha1.ClusterImagePolicy{},
v1alpha1.SchemeGroupVersion.WithKind("TrustRoot"): &v1alpha1.TrustRoot{},
// v1beta1
v1beta1.SchemeGroupVersion.WithKind("ClusterImagePolicy"): &v1beta1.ClusterImagePolicy{},
}
Expand Down Expand Up @@ -103,6 +105,7 @@ func main() {
// This calls flag.Parse()
sharedmain.MainWithContext(ctx, "clusterimagepolicy",
certificates.NewController,
trustroot.NewController,
clusterimagepolicy.NewController,
NewPolicyValidatingAdmissionController,
NewPolicyMutatingAdmissionController,
Expand Down
1 change: 1 addition & 0 deletions cmd/schema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
// schema is a tool to dump the schema for policy-controller resources.
func main() {
registry.Register(&v1alpha1.ClusterImagePolicy{})
registry.Register(&v1alpha1.TrustRoot{})
registry.Register(&v1beta1.ClusterImagePolicy{})

if err := commands.New("github.com/sigstore/policy-controller").Execute(); err != nil {
Expand Down
9 changes: 8 additions & 1 deletion config/200-clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,18 @@ rules:
resources: ["customresourcedefinitions"]
verbs: ["get", "update"]
resourceNames: ["clusterimagepolicies.policy.sigstore.dev"]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "update"]
resourceNames: ["trustroots.policy.sigstore.dev"]

# Allow reconciliation of the ClusterImagePolic CRDs.
# Allow reconciliation of the ClusterImagePolicy and TrustRoot CRDs.
- apiGroups: ["policy.sigstore.dev"]
resources: ["clusterimagepolicies"]
verbs: ["get", "list", "update", "watch", "patch"]
- apiGroups: ["policy.sigstore.dev"]
resources: ["trustroots"]
verbs: ["get", "list", "update", "watch", "patch"]

# This is needed by k8schain to support fetching pull secrets attached to pod specs
# or their service accounts. If pull secrets aren't used, the "secrets" below can
Expand Down
7 changes: 7 additions & 0 deletions config/200-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,10 @@ rules:
resources: ["configmaps"]
resourceNames: ["config-image-policies"]
verbs: ["get", "list", "create", "update", "patch", "watch"]

# This is needed to create / patch ConfigMap that is created by the reconciler
# to consolidate various TrustRoot configuration into SigstoreKeys ConfigMap.
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["config-sigstore-keys"]
verbs: ["get", "list", "create", "update", "patch", "watch"]
144 changes: 144 additions & 0 deletions config/300-trustroot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Copyright 2022 The Sigstore 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
#
# https://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.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: trustroots.policy.sigstore.dev
spec:
conversion:
strategy: None
group: policy.sigstore.dev
names:
kind: TrustRoot
plural: trustroots
singular: trustroot
categories:
- all
- sigstore
scope: Cluster
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
description: Spec is the definition for a trust root. This is either a TUF root and remote or local repository. You can also bring your own keys/certs here.
type: object
properties:
remote:
description: Remote specifies initial root of trust & remote mirror.
type: object
properties:
mirror:
description: 'Mirror is the remote mirror, for example: https://sigstore-tuf-root.storage.googleapis.com'
type: string
root:
description: Root is the json encoded trusted initial root.
type: string
repository:
description: Repository contains the serialized TUF remote repository.
type: object
properties:
mirrorFS:
description: MirrorFS is the base64 tarred, gzipped, and base64 encoded remote repository that can be used for example in air-gap environments. Will not make outbound network connections, and must then be kept up to date in some other manner. The repository must contain metadata as well as targets.
type: string
root:
description: Root is the json encoded trusted initial root.
type: string
targets:
description: Targets is where the targets live off of the root of the Repository above. If not specified 'targets' is defaulted.
type: string
sigstoreKeys:
description: SigstoreKeys contains the serialized keys.
type: object
properties:
certificateAuthorities:
description: Trusted certificate authorities (e.g Fulcio).
type: array
items:
type: object
properties:
certChain:
description: The certificate chain for this CA in PEM format. Last entry in this chain is the Root certificate.
type: string
subject:
description: The root certificate MUST be self-signed, and so the subject and issuer are the same.
type: object
properties:
commonName:
type: string
organization:
type: string
uri:
description: The URI at which the CA can be accessed.
type: string
ctLogs:
description: Certificate Transparency Log
type: array
items:
type: object
properties:
baseURL:
description: The base URL which can be used for URLs for clients.
type: string
hashAlgorithm:
description: / The hash algorithm used for the Merkle Tree
type: string
logID:
description: The unique identifier for this transparency log.
type: string
publicKey:
description: PEM encoded public key
type: string
tLogs:
description: Rekor log specifications
type: array
items:
type: object
properties:
baseURL:
description: The base URL which can be used for URLs for clients.
type: string
hashAlgorithm:
description: / The hash algorithm used for the Merkle Tree
type: string
logID:
description: The unique identifier for this transparency log.
type: string
publicKey:
description: PEM encoded public key
type: string
timestampAuthorities:
description: Trusted timestamping authorities
type: array
items:
type: object
properties:
certChain:
description: The certificate chain for this CA in PEM format. Last entry in this chain is the Root certificate.
type: string
subject:
description: The root certificate MUST be self-signed, and so the subject and issuer are the same.
type: object
properties:
commonName:
type: string
organization:
type: string
uri:
description: The URI at which the CA can be accessed.
type: string
29 changes: 29 additions & 0 deletions config/config-sigstore-keys.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2022 The Sigstore 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.

apiVersion: v1
kind: ConfigMap
metadata:
name: config-sigstore-keys
namespace: cosign-system

data:
_example: |
################################
# #
# EXAMPLE CONFIGURATION #
# #
################################
my-custom-sigstore-keys: |-
{"certificateAuthority":[{"subject":{"organization":"fulcio-organization","commonName":"fulcio-common-name"},"uri":"https://fulcio.example.com","certChain":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCglNSUlGd3pDQ0E2dWdBd0lCQWdJSUs3eGIrcnFZNGdFd0RRWUpLb1pJaHZjTkFRRUxCUUF3ZmpFTU1Bb0dBMVVFCglCaE1EVlZOQk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFUWVc0Z1JuSmhibU5wCgljMk52TVJZd0ZBWURWUVFKRXcwMU5EZ2dUV0Z5YTJWMElGTjBNUTR3REFZRFZRUVJFd1UxTnpJM05ERVpNQmNHCglBMVVFQ2hNUVRHbHVkWGdnUm05MWJtUmhkR2x2YmpBZUZ3MHlNakV5TURnd01qRTNOVEZhRncweU16RXlNRGd3CglNakUzTlRGYU1INHhEREFLQmdOVkJBWVRBMVZUUVRFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHCglBMVVFQnhNTlUyRnVJRVp5WVc1amFYTmpiekVXTUJRR0ExVUVDUk1OTlRRNElFMWhjbXRsZENCVGRERU9NQXdHCglBMVVFRVJNRk5UY3lOelF4R1RBWEJnTlZCQW9URUV4cGJuVjRJRVp2ZFc1a1lYUnBiMjR3Z2dJaU1BMEdDU3FHCglTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDQVFDMTQyRWpsZzJReEl3cE5qYmFlVy9mdDlzSDFUWFU2Q1dnCglic3ZWcDc3dlJnY2tTbnBNM1JUQy9nd0V3Skh0WCtHT1RyUDlybzZuRkpOM0czaGNGbmFNSExLZEdyb2Y5aUh1Cgkvdy9sWkx3UXpYelZUKzBaeVp4eXRIQVdHRkJ2bVlNNEozM2pINkRqOVB2cU9Od3RTQlNtWkJQYy9ILzhFdllzCglVenhQV3VraE90b3RTSDNWWERxWjRqbDk2TUxlMCs1ZzJXaTdNeFJYNDRYMVJpUFMxNGJhMUVTNTM4YlRoaGNRCgk0U01qM3VoYmRzQ0lrY203ZUY0RVkzcEVYUXBYRUVHblpHZndZZ1FyKzZjVDA3WmQvV0RNME5YM0t4SDZxUms5CglnRGpQbmZjTXVGYk9UYmZEL251dng2Rk5YNk9VcnpyWlNnbGtMdmNQSUJWT1c3TG40MUxBYjdhWG1iV0xGRUpuCgl1TG9vUHBZWXIrNk5obkZETkdwc0JLR0tyL2t2YlF5REtLc3QzQ0tqOW90UFMxMzYzbmk0MXFub0E3WVdTcXh3Cgl6NDE4NWRLS2MrWTd5dkpRc1JscjZxRzFzTkxPK2M3N2ZTUzVWWkltek5vekJjUmt1TEpGbFgrV0IwdXpnUVU1CglzNDVJWlcrZks5Mm5mdThNbUtqekhSK2lkeXI0T3lqUzBZU04zR01nYzBVUDdLNmhWcGhMZWRBcEZweWtCU0ZHCglVZ2lQWndyVCttR1NWZ21PWHE1bjFkUVRDRDE0bEVoMnF0My9yZmY4ek5jMENNQU5XeWJhTUdCR1E0YmhWVlhlCglSS1l4OXUyUFpqUHY1M3A3WWIvRENkcW5HRUR3L0hDQkRpQ3M0b1llNGRhRTM2eFVvanhEU20zRGFlTkc2OHo5CglSTDdnZlVqQXhRSURBUUFCbzBVd1F6QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0VnWURWUjBUQVFIL0JBZ3dCZ0VCCgkvd0lCQVRBZEJnTlZIUTRFRmdRVWYrbGJOWDBXaDRoK1EwU1J0aFJLK0tmTGpxRXdEUVlKS29aSWh2Y05BUUVMCglCUUFEZ2dJQkFFaEpqYTBaU0t3WGNhT1hDWVJYVEUwNitKYnBlekk1TGV2QmhtYlJRSzc4OVJxMTBKZUFYYTdtCglFVG9SR2xHRkxIMnVEVDExbXNGS3lNM3Y2N0tsRTFTWVZjcUttQ2xZZklWRVlIM0xhMHVJKzlySFpuV2diNEJsCgl5MUI4d2JsS0p6aFlRRDlaNEgvZ3MrQkFzb1JYNVZvRnlJZ2tOQmsxcDNmdGFWQ2JrUXZTME9ZdFlzNWl3NGVLCgljSTcxL0lzVElUM1pwcGo5UjhJR3Nxd0xLZ3pmbnlOY0ZKZHorb2hjNlYyMlBqWk1FQkhDc0hQTzRhdjJMbFdLCgk1WTFmbEwrMmJxVHFibU8vYmpmWDB3NFoxRHVvalJjT1pGN1NINE8zUXUyWTcvNjlnSDdDcDBuaVZDbTV6K1M1CgkwMTFWNlB2TWpybWlFK3hWa3hMSGJZRWdvY2JGaGQ1RGNpTUNYcHZzdURab2phSTNGUkVtQnFpSWhLb2tpM3JiCgl3dUVseWE3OGJNd2taMWtycDc2bldzbzQ3LzArNTFpby9XcmlBZHIwY2ptem9uaG83UnFJRTNEQzc3Q0VNa2FnCgladktTbUwzc2ZmK1dOU3JuUGx6bksxOU5BMno0SW1XOU1zenFQckNUUUdQLy9CQnU3U2Ftem9mVk05ZjRQQUlyCglGVHBuVzZzR2RwQ3pQOEUwV1V1OUIrdmlLcnRmTS85c3huSTlXaGZKUGRyRVAwaVpXM3Zod3ZnUWJLYjVEMk9TCglVNG5yVm92NkJXci9CbmhRSzhJWG8xdHEzajhGQ1JJb2xlWE5oa3M0Z25rT2FEc1cyS3RWcXd0SzNpTzNCdlBiCglMNXcwZ2RMandNTGtlazcyeTYxWHF6NVd4WndOaGw1WWNtQkt1U3ZtVlNIdkE2OEJWU2JCCgktLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCgk="}],"tLog":[{"baseURL":"https://rekor.example.com","hashAlgorithm":"sha-256","publicKey":"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KCU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTdEMld2Z3FTenM5anBkSnNPSjVObDZ4ZzhKWG0KCU5tbzdNM2JONytkUWRkdzlJYmMyUjNTVjh0ekJadzByU1Q4RktjbjRhcEplcGNLTTRxVXBZVWVOZnc9PQoJLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCgk=","logID":"rekor-log-id"}],"ctLog":[{"baseURL":"https://ctfe.example.com","hashAlgorithm":"sha-256","publicKey":"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KCU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUp2Q0ppNzA3ZnY1dE1KMVUyVFZNWit1TzRkS0cKCWFFY3ZqbENrZ0JDS1hicmt1bVpWMG0wZFNsSzFWMWd4RWl5UTh5NmhrMU14Sk5lMkFaclpVdDdhNHc9PQoJLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCgk=","logID":"ctfe-log-id"}],"timestampAuthorities":[{"subject":{"organization":"tsa-organization","commonName":"tsa-common-name"},"uri":"https://tsa.example.com","certChain":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCglNSUlCekRDQ0FYS2dBd0lCQWdJVWZ5R0tEb0ZhN3k2cy9XMXAxQ2lUbUJSczFlQXdDZ1lJS29aSXpqMEVBd0l3CglNREVPTUF3R0ExVUVDaE1GYkc5allXd3hIakFjQmdOVkJBTVRGVlJsYzNRZ1ZGTkJJRWx1ZEdWeWJXVmthV0YwCglaVEFlRncweU1qRXhNRGt5TURNeE16UmFGdzB6TVRFeE1Ea3lNRE0wTXpSYU1EQXhEakFNQmdOVkJBb1RCV3h2CglZMkZzTVI0d0hBWURWUVFERXhWVVpYTjBJRlJUUVNCVWFXMWxjM1JoYlhCcGJtY3dXVEFUQmdjcWhrak9QUUlCCglCZ2dxaGtqT1BRTUJCd05DQUFSM0tjRHk5andBUlgwckR2eXIrTUdHa0czbjFPQTBNVTUrWmlEbWd1c0Z5azZVCgk2Ym92S1dWTWZEOEo4TlRjSlpFMFJhWUpyOC9kRTlrZ2NJSVhsaE13bzJvd2FEQU9CZ05WSFE4QkFmOEVCQU1DCglCNEF3SFFZRFZSME9CQllFRkhObjVSM2IzTXRVZFNOckZPNDlRNlhEVlNua01COEdBMVVkSXdRWU1CYUFGTkxTCgk2Z25vN09tKytRdDV6SWErSDlvMEhpVDJNQllHQTFVZEpRRUIvd1FNTUFvR0NDc0dBUVVGQndNSU1Bb0dDQ3FHCglTTTQ5QkFNQ0EwZ0FNRVVDSVFDRjBvbG9obnZkVXE2VDcvd1BrMTlaNWFRUC95eFJUakNXWXVobi9UQ3lIZ0lnCglhelYzYWlyNEdSWmJOOWJkWXRjUTdKVUFLcTg5R09odEZmbDZrY29WVXZVPQoJLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQoJLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCglNSUlCMGpDQ0FYaWdBd0lCQWdJVVhwQm1ZSkZGYUdXM2NDOHA2Yi9ESHIxaThJb3dDZ1lJS29aSXpqMEVBd0l3CglLREVPTUF3R0ExVUVDaE1GYkc5allXd3hGakFVQmdOVkJBTVREVlJsYzNRZ1ZGTkJJRkp2YjNRd0hoY05Nakl4CglNVEE1TWpBeU9UTTBXaGNOTXpJeE1UQTVNakF6TkRNMFdqQXdNUTR3REFZRFZRUUtFd1ZzYjJOaGJERWVNQndHCglBMVVFQXhNVlZHVnpkQ0JVVTBFZ1NXNTBaWEp0WldScFlYUmxNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBECglBUWNEUWdBRUtEUERSSXdEUzFaQ3ltdWI2eWFuQ0c1bWEwcURqTHBOb25Edm9vU2tSSEVnVTBUTmliZUpuNk0rCgk1VzYwOGhDdzhud3V1Y01iWFE0MWtOZXVCZWV2eXFONE1IWXdEZ1lEVlIwUEFRSC9CQVFEQWdFR01CTUdBMVVkCglKUVFNTUFvR0NDc0dBUVVGQndNSU1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZOTFM2Z25vCgk3T20rK1F0NXpJYStIOW8wSGlUMk1COEdBMVVkSXdRWU1CYUFGQjFudlhwTks3QXVRbGJKK3lhNm5QU3FXaStUCglNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJR2l3cUNJMjl3N0M0VjhUbHRDc2k3MjhzNUR0a2xDUHlTREFTVVN1CglhNXk1QWlFQTQwSWZkbHdmN1VqOHE4TlNENlo0Zy8wanMwdEdOZExTVUoxZG8vV29OMHM9CgktLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCgktLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KCU1JSUJsRENDQVRxZ0F3SUJBZ0lVWVp4OXNTMTRFbjdTdUhET0pKUDRJUG9wTWpVd0NnWUlLb1pJemowRUF3SXcKCUtERU9NQXdHQTFVRUNoTUZiRzlqWVd3eEZqQVVCZ05WQkFNVERWUmxjM1FnVkZOQklGSnZiM1F3SGhjTk1qSXgKCU1UQTVNakF5T1RNMFdoY05Nekl4TVRBNU1qQXpORE0wV2pBb01RNHdEQVlEVlFRS0V3VnNiMk5oYkRFV01CUUcKCUExVUVBeE1OVkdWemRDQlVVMEVnVW05dmREQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJBYkIKCUIwU1U4Rzc1aFZJVXBoQ2hBNG5mT3dOV1AzNDdUalNjSWRzRVByS1ZuKy9ZMUhtbUxISkRqU2ZuK3hoRUZvRWsKCTdqcWdycW9uNDhpNHhibzd4QXVqUWpCQU1BNEdBMVVkRHdFQi93UUVBd0lCQmpBUEJnTlZIUk1CQWY4RUJUQUQKCUFRSC9NQjBHQTFVZERnUVdCQlFkWjcxNlRTdXdMa0pXeWZzbXVwejBxbG92a3pBS0JnZ3Foa2pPUFFRREFnTkkKCUFEQkZBaUJlNVA1NmZvcW1GY1pBVnBFZUFPRlpyQWxFaXEwNUNDcE1OWWg1RWpMdm1BSWhBS05GNnhJVjV1RmQKCXBTVEpzQXd6alc3OENLUW03cW9sMHVQbVBQdTZtTmF3CgktLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t"}]}
Loading