Skip to content
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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.0.30]

### Added

- Support for CircleCI attestations ([#166](https://github.com/pypi/pypi-attestations/pull/166/)).
This is work aligned with adding CircleCI as a Trusted Publisher in pypi. ([#19349](https://github.com/pypi/warehouse/pull/19349))

## [0.0.29]

### Added
Expand Down Expand Up @@ -158,7 +165,7 @@ This is a corrective release for [0.0.18].
- The `GitLabPublisher` policy now takes the workflow file path in order to
verify attestations, rathen than assuming it will always be `gitlab-ci.yml`
([#71](https://github.com/pypi/pypi-attestations/pull/71)).
- The `GitLabPublisher` now longer expects claims being passed during construction,
- The `GitLabPublisher` no longer expects claims being passed during construction,
rather the `ref` and `sha` claims are extracted from the certificate's extensions,
similar to `GitHubPublisher`'s behavior
([#71](https://github.com/pypi/pypi-attestations/pull/71)).
Expand Down
4 changes: 3 additions & 1 deletion src/pypi_attestations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""The `pypi-attestations` APIs."""

__version__ = "0.0.29"
__version__ = "0.0.30"

from ._impl import (
Attestation,
AttestationBundle,
AttestationError,
AttestationType,
CircleCIPublisher,
ConversionError,
Distribution,
Envelope,
Expand All @@ -25,6 +26,7 @@
"AttestationBundle",
"AttestationError",
"AttestationType",
"CircleCIPublisher",
"ConversionError",
"Distribution",
"Envelope",
Expand Down
35 changes: 35 additions & 0 deletions src/pypi_attestations/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from pypi_attestations import Attestation, AttestationError, VerificationError, __version__
from pypi_attestations._impl import (
CircleCIPublisher,
ConversionError,
Distribution,
GitHubPublisher,
Expand Down Expand Up @@ -165,6 +166,18 @@ def _parser() -> argparse.ArgumentParser:
help="Email of the Google Cloud service account",
)

verify_pypi_command.add_argument(
"--circleci-project-id",
type=str,
help="CircleCI project ID (UUID)",
)

verify_pypi_command.add_argument(
"--circleci-pipeline-definition-id",
type=str,
help="CircleCI pipeline definition ID (UUID)",
)

verify_pypi_command.add_argument(
"--staging",
action="store_true",
Expand Down Expand Up @@ -597,6 +610,28 @@ def _verify_pypi(args: argparse.Namespace) -> None:
f"Verification failed: provenance was signed by service account "
f'"{publisher.email}", expected "{args.gcp_service_account}"'
)
elif isinstance(publisher, CircleCIPublisher):
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh - not entirely clear how to test this properly. (help wanted)

In examining the payload from other packages, I can see the publisher comes as part of the call https://pypi.org/integrity/package/version/filename/provenance but not clear what I should be validating against? Would it be enough to just use one of the claims? eg: the project id?

note to self: this value thats being checked maps to the values of attestation_identity. eg: https://github.com/pypi/warehouse/blob/5b980b74b3eee5e79580dc4672abad3d93f14472/warehouse/oidc/models/github.py#L306

I guess what I am wondering about is whats needed here to verify? Like is just the project_id enough? or would i want to use the pipeline_id as well? Unless I misunderstand, after this stage there should be further certificate validation happening of the fulcio cert?

Any guidance here is welcome :D

Copy link
Copy Markdown
Contributor Author

@meeech meeech Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see what I did for warehouse here - meeech/warehouse#1

This is a pr stacked off my pr for adding circleci as a trusted publisher (it wouldnt let me open a pr on the parent against a pr)

if not args.circleci_project_id:
_die(
"Provenance signed by CircleCI, but no project ID provided; "
"use '--circleci-project-id'"
)
if not args.circleci_pipeline_definition_id:
_die(
"Provenance signed by CircleCI, but no pipeline definition ID provided; "
"use '--circleci-pipeline-definition-id'"
)
if publisher.project_id != args.circleci_project_id:
_die(
f"Verification failed: provenance was signed by CircleCI project "
f'"{publisher.project_id}", expected "{args.circleci_project_id}"'
)
if publisher.pipeline_definition_id != args.circleci_pipeline_definition_id:
_die(
f"Verification failed: provenance was signed by CircleCI pipeline "
f'definition "{publisher.pipeline_definition_id}", expected '
f'"{args.circleci_pipeline_definition_id}"'
)
else:
if not args.repository:
_die(
Expand Down
100 changes: 99 additions & 1 deletion src/pypi_attestations/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,105 @@ def _as_policy(self) -> VerificationPolicy:
return policy.Identity(identity=self.email, issuer="https://accounts.google.com")


_Publisher = GitHubPublisher | GitLabPublisher | GooglePublisher
class _CircleCITrustedPublisherPolicy:
"""A custom sigstore-python policy for verifying against a CircleCI-based Trusted Publisher."""

def __init__(
self,
project_id: str,
pipeline_definition_id: str,
vcs_origin: str | None = None,
vcs_ref: str | None = None,
) -> None:
self._project_id = project_id
self._pipeline_definition_id = pipeline_definition_id
self._vcs_origin = vcs_origin
self._vcs_ref = vcs_ref

# Build subpolicies: the issuer must be CircleCI.
subpolicies: list[policy.VerificationPolicy] = [
policy.OIDCIssuerV2("https://oidc.circleci.com")
]

# If vcs_origin is specified, verify the source repository URI matches.
# CircleCI's source-repository-uri comes from oidc.circleci.com/vcs-origin
# and looks like "github.com/org/repo" (without https://).
if self._vcs_origin is not None:
subpolicies.append(policy.OIDCSourceRepositoryURI(self._vcs_origin))

# If vcs_ref is specified, verify the source repository ref matches.
# CircleCI's source-repository-ref comes from oidc.circleci.com/vcs-ref
# and looks like "refs/heads/main".
if self._vcs_ref is not None:
subpolicies.append(policy.OIDCSourceRepositoryRef(self._vcs_ref))

self._subpolicy = policy.AllOf(subpolicies)

def verify(self, cert: Certificate) -> None:
"""Verify the certificate against the Trusted Publisher identity."""
self._subpolicy.verify(cert)

# Extract and verify the Build Signer URI.
# For CircleCI, the Build Signer URI looks like:
# https://circleci.com/api/v2/projects/<project-id>/pipeline-definitions/<pipeline-definition-id>
build_signer_uri = cert.extensions.get_extension_for_oid(
policy._OIDC_BUILD_SIGNER_URI_OID # noqa: SLF001
)
raw_build_signer_uri = _der_decode_utf8string(build_signer_uri.value.public_bytes())

expected_build_signer_uri = (
f"https://circleci.com/api/v2/projects/{self._project_id}/"
f"pipeline-definitions/{self._pipeline_definition_id}"
)
if raw_build_signer_uri != expected_build_signer_uri:
raise sigstore.errors.VerificationError(
f"Certificate's Build Signer URI ({raw_build_signer_uri}) does not match expected "
f"Trusted Publisher ({expected_build_signer_uri})"
)


class CircleCIPublisher(_PublisherBase):
"""A CircleCI-based Trusted Publisher."""

kind: Literal["CircleCI"] = "CircleCI"

project_id: str
"""
The CircleCI project ID (UUID) that performed the publishing action.
"""

pipeline_definition_id: str
"""
The CircleCI pipeline definition ID (UUID) that defines the pipeline configuration.
This uniquely identifies the specific pipeline definition allowed to publish.
"""

vcs_origin: str | None = None
"""
The optional source repository URI that triggered the pipeline.
This comes from the oidc.circleci.com/vcs-origin claim and looks like
"github.com/org/repo" (without the https:// prefix).
Not present for pipelines triggered by custom webhooks.
"""

vcs_ref: str | None = None
"""
The optional git ref that triggered the pipeline.
This comes from the oidc.circleci.com/vcs-ref claim and looks like
"refs/heads/main" or "refs/tags/v1.0.0".
Not present for pipelines triggered by custom webhooks.
"""

def _as_policy(self) -> VerificationPolicy:
return _CircleCITrustedPublisherPolicy(
self.project_id,
self.pipeline_definition_id,
self.vcs_origin,
self.vcs_ref,
)


_Publisher = GitHubPublisher | GitLabPublisher | GooglePublisher | CircleCIPublisher
Publisher = Annotated[_Publisher, Field(discriminator="kind")]


Expand Down
30 changes: 30 additions & 0 deletions test/assets/circleci-oidc.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFNTCCBLugAwIBAgIULp9T1NYp2u3V7BSs39/sj1qAX2IwCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjYwMjA1MjI1OTQyWhcNMjYwMjA1MjMwOTQyWjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAErXNU0vpJVLQwirAtf3wiHEXXdAjQb/hDcRE7
k/2Hj+q8mCIGv6tcdr0Z/40EwzEA1yQe2qT0kxgBtHDj/wQnD6OCA9owggPWMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUUCIM
qzZEPg1+PClZojjElZP1BdYwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wgZQGA1UdEQEB/wSBiTCBhoaBg2h0dHBzOi8vY2lyY2xlY2kuY29tL2FwaS92
Mi9wcm9qZWN0cy9mZGQ5MjgzZi1lNjE5LTQ2YWYtOGY5Yy04NTFmN2QzZThiMmIv
cGlwZWxpbmUtZGVmaW5pdGlvbnMvOGU0ZjhhYjItOGQ3Yy00ODI3LTlmMTUtZGUw
NzZkNmQ2NDdmMCcGCisGAQQBg78wAQEEGWh0dHBzOi8vb2lkYy5jaXJjbGVjaS5j
b20wKQYKKwYBBAGDvzABCAQbDBlodHRwczovL29pZGMuY2lyY2xlY2kuY29tMIGV
BgorBgEEAYO/MAEJBIGGDIGDaHR0cHM6Ly9jaXJjbGVjaS5jb20vYXBpL3YyL3By
b2plY3RzL2ZkZDkyODNmLWU2MTktNDZhZi04ZjljLTg1MWY3ZDNlOGIyYi9waXBl
bGluZS1kZWZpbml0aW9ucy84ZTRmOGFiMi04ZDdjLTQ4MjctOWYxNS1kZTA3NmQ2
ZDY0N2YwEgYKKwYBBAGDvzABCwQEDAIiIjBEBgorBgEEAYO/MAEMBDYMNGdpdGh1
Yi5jb20vQ2lyY2xlQ0ktUHVibGljL3NpZ24tYW5kLXB1Ymxpc2gtZXhhbXBsZXMw
HwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL3B5cGkwYAYKKwYBBAGDvzABEgRS
DFBodHRwczovL2NpcmNsZWNpLmNvbS9hcGkvdjIvcGlwZWxpbmUvZjNmNmViODYt
ZjI2MC00NDViLWI5YzktMzJhMmNiNWEzYTYxL2NvbmZpZzCBgAYKKwYBBAGDvzAB
FQRyDHBodHRwczovL2FwcC5jaXJjbGVjaS5jb20vL3dvcmtmbG93LzYxNzE4ZDAy
LWNlMjQtNGYxZi1iYzhlLWFhZDUzODMwNzMwMi9qb2IvOGJkY2RlZDUtY2RjMC00
NTIxLTg0NDctYmIyYjMxOWM2MWM2MIGLBgorBgEEAdZ5AgQCBH0EewB5AHcA3T0w
asbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGcMAhj1gAABAMASDBGAiEA
zWiAEWa3YopYyEe8sR7ldJaktrymd9CI2YjCXf2fOowCIQC+A+eWVVTXVx/Ysp3l
QjnRE7sRlOZqNHQMQYqiKdPTfjAKBggqhkjOPQQDAwNoADBlAjEA3E5hk+y1VoCm
T8dW38ISnOrRySfBo5R6EDuqfbk5fY5mmfKODaxkAN377srxCTVrAjAYzIc5Wx6c
SK8xta+GoskdR74qdDF/VPMmMkfldqy6DTkmUkbtCRD/cZPg1Bzi5S8=
-----END CERTIFICATE-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":1,"attestation_bundles":[{"publisher":{"kind":"CircleCI","project_id":"fdd9283f-e619-46af-8f9c-851f7d3e8b2b","pipeline_definition_id":"8e4f8ab2-8d7c-4827-9f15-de076d6d647f","vcs_origin":"github.com/CircleCI-Public/sign-and-publish-examples","vcs_ref":"refs/heads/pypi"},"attestations":[{"version":1,"verification_material":{"certificate":"MIIFNTCCBLugAwIBAgIULp9T1NYp2u3V7BSs39/sj1qAX2IwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwMjA1MjI1OTQyWhcNMjYwMjA1MjMwOTQyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErXNU0vpJVLQwirAtf3wiHEXXdAjQb/hDcRE7k/2Hj+q8mCIGv6tcdr0Z/40EwzEA1yQe2qT0kxgBtHDj/wQnD6OCA9owggPWMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUUCIMqzZEPg1+PClZojjElZP1BdYwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wgZQGA1UdEQEB/wSBiTCBhoaBg2h0dHBzOi8vY2lyY2xlY2kuY29tL2FwaS92Mi9wcm9qZWN0cy9mZGQ5MjgzZi1lNjE5LTQ2YWYtOGY5Yy04NTFmN2QzZThiMmIvcGlwZWxpbmUtZGVmaW5pdGlvbnMvOGU0ZjhhYjItOGQ3Yy00ODI3LTlmMTUtZGUwNzZkNmQ2NDdmMCcGCisGAQQBg78wAQEEGWh0dHBzOi8vb2lkYy5jaXJjbGVjaS5jb20wKQYKKwYBBAGDvzABCAQbDBlodHRwczovL29pZGMuY2lyY2xlY2kuY29tMIGVBgorBgEEAYO/MAEJBIGGDIGDaHR0cHM6Ly9jaXJjbGVjaS5jb20vYXBpL3YyL3Byb2plY3RzL2ZkZDkyODNmLWU2MTktNDZhZi04ZjljLTg1MWY3ZDNlOGIyYi9waXBlbGluZS1kZWZpbml0aW9ucy84ZTRmOGFiMi04ZDdjLTQ4MjctOWYxNS1kZTA3NmQ2ZDY0N2YwEgYKKwYBBAGDvzABCwQEDAIiIjBEBgorBgEEAYO/MAEMBDYMNGdpdGh1Yi5jb20vQ2lyY2xlQ0ktUHVibGljL3NpZ24tYW5kLXB1Ymxpc2gtZXhhbXBsZXMwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL3B5cGkwYAYKKwYBBAGDvzABEgRSDFBodHRwczovL2NpcmNsZWNpLmNvbS9hcGkvdjIvcGlwZWxpbmUvZjNmNmViODYtZjI2MC00NDViLWI5YzktMzJhMmNiNWEzYTYxL2NvbmZpZzCBgAYKKwYBBAGDvzABFQRyDHBodHRwczovL2FwcC5jaXJjbGVjaS5jb20vL3dvcmtmbG93LzYxNzE4ZDAyLWNlMjQtNGYxZi1iYzhlLWFhZDUzODMwNzMwMi9qb2IvOGJkY2RlZDUtY2RjMC00NTIxLTg0NDctYmIyYjMxOWM2MWM2MIGLBgorBgEEAdZ5AgQCBH0EewB5AHcA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGcMAhj1gAABAMASDBGAiEAzWiAEWa3YopYyEe8sR7ldJaktrymd9CI2YjCXf2fOowCIQC+A+eWVVTXVx/Ysp3lQjnRE7sRlOZqNHQMQYqiKdPTfjAKBggqhkjOPQQDAwNoADBlAjEA3E5hk+y1VoCmT8dW38ISnOrRySfBo5R6EDuqfbk5fY5mmfKODaxkAN377srxCTVrAjAYzIc5Wx6cSK8xta+GoskdR74qdDF/VPMmMkfldqy6DTkmUkbtCRD/cZPg1Bzi5S8=","transparency_entries":[{"logIndex":"920610516","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1770332382","inclusionPromise":{"signedEntryTimestamp":"MEQCIDQ6DWCsKA0dOsV9ve49j/1I8Pjl5OEVtbmWdcVY06p4AiAQ3443NpjtNahdNASponvgIDWtH5sabdIKFuH2ffKAdg=="},"inclusionProof":{"logIndex":"798706254","rootHash":"lMmsE8xSERAgev2Giq1b50My88qPNBHSkgmczJxrc6o=","treeSize":"798706265","hashes":["oE24j8HDy39EcGPGUbeaLw7Bp05OzXjlayiicbqsFac=","iEHRNAppbto2qwl3Ixx2f4HGDu8Mx63NzS/AddcCPzs=","87T+1bf0XzK5O27W5xazGJcORi9QImpqUJOyAy4lgD8=","/ZaTOVC+voMYrlDR0BTZ02sUXxekLI8Rf9U3tl/v2/A=","OASG+jtmGyVG02/0YPw6WgtL6m/DLTQkmEt8nPRLM2U=","ko7NXRXRiLNPlz8jOsuOA7w8CySoGY4DjjCjJoaxD1Y=","KP690oGEMlx/EbpbwCNLZQuBSrQB5tn2v/Klp+f4jHk=","VG3VSd2N1IaasHVoOVncBfwKFc5OZn6QSGlqnXOphGk=","UQKAfR/g0D6p3yoSh5fR5eDK1xsoOFh3wuGaLb2AWsU=","3HrtfmjmLFfvcJE3nFp00HzXxv9gEFoxfm6vT6FBn8s=","qaqTstsK3m23uyXy/dLOZXptc6/eHozznMQSqm0RQOg=","9akCuzCiy3l4Ud+6GFWGaiqzxtJb+qGCCaQJudDYC+M=","5U1oz++Vq3x6JzGQlGUJdxG7EQq7MuvPLOBovvc44qg=","Rv1foHpQREdo7jf8pCzOAuCgJrvXl2391yMda8VbuO8=","SNddWrbJR3hEJjCdUngL2L2kJ3vfPiwwHboa5Qi/QEo=","yeCWAa93hha1YBKuFn93zBzKbqQW3tYHrgkSp5U7ndU=","4O6YxKguFZGEr7Xsa3hqNAN2Qq7uVVat/IV4masT570=","F9MSQ5SmoFr+hoADclpdFY52/TLfHDnNPYb9ZNYO5gI=","T4DqWD42hAtN+vX8jKCWqoC4meE4JekI9LxYGCcPy1M="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n798706265\nlMmsE8xSERAgev2Giq1b50My88qPNBHSkgmczJxrc6o=\n\n— rekor.sigstore.dev wNI9ajBFAiB5P2IWqWj/0juWh85rijRWv+Yijpb7L8H6dfn/NMgIZgIhAOM9TrUf9OtOMidGnHudHEAU0ZfA9f+aN5mUtdhlKF34\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiMGEyODQxZTUyN2ZiZDNmY2E0MGQxYmY3Y2Y3YWViMzk4NGMxMTAyOWQ5MmJjMGJlOTQxZmE1NDY3Y2FiNTZjZCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImU2YTMzYTMyYWJjYjcxNDE2ZjE3MDllMzU1ZjAzMmI4ZTdmMDZkZWE5MGI3ZDJjMmMyNzA1ZmY2OTlhODA3MGQifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRQ0ZWV0lHeGNrVjQxTk9YTWZFNVdFM2tYNHJwUVZsRU5CVnVDTnhlcDNCZXdJZ1BqM0xBVWJSQStBaCtqNzZreHFRSkNBSnBxY2lZdnF1RnJLZ0t1K3JJMnc9IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VaT1ZFTkRRa3gxWjBGM1NVSkJaMGxWVEhBNVZERk9XWEF5ZFROV04wSlRjek01TDNOcU1YRkJXREpKZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwWmQwMXFRVEZOYWtreFQxUlJlVmRvWTA1TmFsbDNUV3BCTVUxcVRYZFBWRkY1VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVnlXRTVWTUhad1NsWk1VWGRwY2tGMFpqTjNhVWhGV0Zoa1FXcFJZaTlvUkdOU1JUY0theTh5U0dvcmNUaHRRMGxIZGpaMFkyUnlNRm92TkRCRmQzcEZRVEY1VVdVeWNWUXdhM2huUW5SSVJHb3ZkMUZ1UkRaUFEwRTViM2RuWjFCWFRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVlZRMGxOQ25GNldrVlFaekVyVUVOc1dtOXFha1ZzV2xBeFFtUlpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMmRhVVVkQk1WVmtSVkZGUWk5M1UwSnBWRU5DYUc5aFFtY3lhREJrU0VKNlQyazRkbGt5YkhsWk1uaHNXVEpyZFZreU9YUk1Na1ozWVZNNU1ncE5hVGwzWTIwNWNWcFhUakJqZVRsdFdrZFJOVTFxWjNwYWFURnNUbXBGTlV4VVVUSlpWMWwwVDBkWk5WbDVNRFJPVkVadFRqSlJlbHBVYUdsTmJVbDJDbU5IYkhkYVYzaHdZbTFWZEZwSFZtMWhWelZ3WkVkc2RtSnVUWFpQUjFVd1dtcG9hRmxxU1hSUFIxRXpXWGt3TUU5RVNUTk1WR3h0VFZSVmRGcEhWWGNLVG5wYWEwNXRVVEpPUkdSdFRVTmpSME5wYzBkQlVWRkNaemM0ZDBGUlJVVkhWMmd3WkVoQ2VrOXBPSFppTW14cldYazFhbUZZU21waVIxWnFZVk0xYWdwaU1qQjNTMUZaUzB0M1dVSkNRVWRFZG5wQlFrTkJVV0pFUW14dlpFaFNkMk42YjNaTU1qbHdXa2ROZFZreWJIbFpNbmhzV1RKcmRWa3lPWFJOU1VkV0NrSm5iM0pDWjBWRlFWbFBMMDFCUlVwQ1NVZEhSRWxIUkdGSVVqQmpTRTAyVEhrNWFtRllTbXBpUjFacVlWTTFhbUl5TUhaWldFSndURE5aZVV3elFua0tZakp3YkZrelVucE1NbHByV2tScmVVOUVUbTFNVjFVeVRWUnJkRTVFV21oYWFUQTBXbXBzYWt4VVp6Rk5WMWt6V2tST2JFOUhTWGxaYVRsM1lWaENiQXBpUjJ4MVdsTXhhMXBYV25CaWJXd3dZVmM1ZFdONU9EUmFWRkp0VDBkR2FVMXBNRFJhUkdScVRGUlJORTFxWTNSUFYxbDRUbE14YTFwVVFUTk9iVkV5Q2xwRVdUQk9NbGwzUldkWlMwdDNXVUpDUVVkRWRucEJRa04zVVVWRVFVbHBTV3BDUlVKbmIzSkNaMFZGUVZsUEwwMUJSVTFDUkZsTlRrZGtjR1JIYURFS1dXazFhbUl5TUhaUk1teDVXVEo0YkZFd2EzUlZTRlpwWWtkc2Frd3pUbkJhTWpSMFdWYzFhMHhZUWpGWmJYaHdZekpuZEZwWWFHaGlXRUp6V2xoTmR3cElkMWxMUzNkWlFrSkJSMFIyZWtGQ1JHZFJVa1JCT1hsYVYxcDZUREpvYkZsWFVucE1NMEkxWTBkcmQxbEJXVXRMZDFsQ1FrRkhSSFo2UVVKRloxSlRDa1JHUW05a1NGSjNZM3B2ZGt3eVRuQmpiVTV6V2xkT2NFeHRUblppVXpsb1kwZHJkbVJxU1haalIyeDNXbGQ0Y0dKdFZYWmFhazV0VG0xV2FVOUVXWFFLV21wSk1rMURNREJPUkZacFRGZEpOVmw2YTNSTmVrcG9UVzFPYVU1WFJYcFpWRmw0VERKT2RtSnRXbkJhZWtOQ1owRlpTMHQzV1VKQ1FVZEVkbnBCUWdwR1VWSjVSRWhDYjJSSVVuZGplbTkyVERKR2QyTkROV3BoV0VwcVlrZFdhbUZUTldwaU1qQjJURE5rZG1OdGRHMWlSemt6VEhwWmVFNTZSVFJhUkVGNUNreFhUbXhOYWxGMFRrZFplRnBwTVdsWmVtaHNURmRHYUZwRVZYcFBSRTEzVG5wTmQwMXBPWEZpTWtsMlQwZEthMWt5VW14YVJGVjBXVEpTYWsxRE1EQUtUbFJKZUV4VVp6Qk9SR04wV1cxSmVWbHFUWGhQVjAweVRWZE5NazFKUjB4Q1oyOXlRbWRGUlVGa1dqVkJaMUZEUWtnd1JXVjNRalZCU0dOQk0xUXdkd3BoYzJKSVJWUktha2RTTkdOdFYyTXpRWEZLUzFoeWFtVlFTek12YURSd2VXZERPSEEzYnpSQlFVRkhZMDFCYUdveFowRkJRa0ZOUVZORVFrZEJhVVZCQ25wWGFVRkZWMkV6V1c5d1dYbEZaVGh6VWpkc1pFcGhhM1J5ZVcxa09VTkpNbGxxUTFobU1tWlBiM2REU1ZGREswRXJaVmRXVmxSWVZuZ3ZXWE53TTJ3S1VXcHVVa1UzYzFKc1QxcHhUa2hSVFZGWmNXbExaRkJVWm1wQlMwSm5aM0ZvYTJwUFVGRlJSRUYzVG05QlJFSnNRV3BGUVRORk5XaHJLM2t4Vm05RGJRcFVPR1JYTXpoSlUyNVBjbEo1VTJaQ2J6VlNOa1ZFZFhGbVltczFabGsxYlcxbVMwOUVZWGhyUVU0ek56ZHpjbmhEVkZaeVFXcEJXWHBKWXpWWGVEWmpDbE5MT0hoMFlTdEhiM05yWkZJM05IRmtSRVl2VmxCTmJVMXJabXhrY1hrMlJGUnJiVlZyWW5SRFVrUXZZMXBRWnpGQ2VtazFVemc5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0="}]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiY2lyY2xlY2lfc2lnbl9wdWJsaXNoX2V4YW1wbGUtMC4wLjEuZGV2MTM3LXB5My1ub25lLWFueS53aGwiLCJkaWdlc3QiOnsic2hhMjU2IjoiOGNiMzRkYTFlMjczMzZmMTFhMmM0M2YzZjljNzQzM2JkNzZmMjRmNDcwYzkwNDcyNGYwNmFkN2YxMzFkOWU5ZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2RvY3MucHlwaS5vcmcvYXR0ZXN0YXRpb25zL3B1Ymxpc2gvdjEiLCJwcmVkaWNhdGUiOm51bGx9","signature":"MEUCIQCFVWIGxckV41NOXMfE5WE3kX4rpQVlENBVuCNxep3BewIgPj3LAUbRA+Ah+j76kxqQJCAJpqciYvquFrKgKu+rI2w="}}]}]}
Loading