Skip to content

Commit b5362c7

Browse files
dkirov-ddclaude
andauthored
feat(downloader): add TUFPointerDownloader for v2 pointer-file format (DataDog#23144)
* feat(downloader): add TUFPointerDownloader for v2 pointer-file format The new agent-integrations-tuf pipeline produces TUF targets as JSON pointer files (targets/<project>/<version>.json) rather than the old HTML simple index + in-toto approach. This commit adds: - TUFPointerDownloader in download_v2.py: TUF-verifies the pointer file, then fetches and sha256-verifies the wheel from S3. - DigestMismatch exception for sha256/length failures. - --format v2 CLI flag: routes through TUFPointerDownloader. --unsafe-disable-verification carries forward; --type and --ignore-python-version are no-ops in v2 with a warning. - 8 offline unit tests covering happy path, missing target, digest mismatch, length mismatch, and disable_verification mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(downloader): use --repository URL for wheel fetch, not pointer's baked value The pointer file always contains the prod S3 repository URL. When validating staging, the caller passes --repository <staging-url> to point at the staging bucket; that URL should be used for both the TUF metadata fetch AND the wheel download, not just the metadata. Adds a test that asserts the wheel is fetched from the caller-supplied URL even when the pointer contains a different (prod) repository value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(downloader): resolve latest via S3 listing, drop latest.json reliance Replace the ``latest.json`` rolling pointer fetch with an S3 ``ListObjectsV2`` walk over ``targets/<project>/``: filter keys to PEP 440 stable versions and pick the maximum. The chosen version is then fetched through TUF as before, so the pointer file the client trusts is still cryptographically verified. Why list S3 instead of parsing the signed targets metadata: once ``path_hash_prefixes`` delegations are in use, a client cannot tell from metadata alone which delegation signs the latest version of a given project. Listing the bucket sidesteps that — TUF still authoritatively verifies the chosen version's pointer. The publisher counterpart in agent-integrations-tuf drops ``latest.json`` entirely; see DataDog/agent-integrations-tuf PR #9. - ``_resolve_latest_version`` lists ``targets/<project>/`` via the S3 REST API (no boto3 dep), parses the XML response, follows the continuation-token pagination, and applies a PEP 440 stable filter - ``get_pointer(project, version=None)`` resolves ``version`` itself before delegating to the TUF Updater - 6 new offline tests cover max-version selection, pre-release/dev filtering, post-release support, the no-stable error, paginated listings, and non-pointer key skipping Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert "refactor(downloader): resolve latest via S3 listing, drop latest.json reliance" This reverts commit 70688d8. * feat(downloader): bundle 1.root.json; rename --format to --index; drop --root-json - Bundle metadata/root_history/1.root.json from agent-integrations-tuf as a package resource; TUFPointerDownloader loads it via importlib.resources — no TOFU, no --root-json flag needed - Rename --format v2 to --index (boolean flag); v1 remains the default when --index is absent - Remove trust_anchor parameter from TUFPointerDownloader.__init__ - Drop --format and --root-json from instantiate_downloader (v1 path) - Register 1.root.json as a wheel artifact in pyproject.toml - Update tests to match new interface Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * fix(downloader): rename --index to --v2 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * feat(downloader): default to v2 with v1 fallback; add prod URL constant Without any flag the downloader now attempts v2 (against the prod S3 bucket) and falls back to v1 on any failure, so callers get the new format automatically without code changes. Passing --v2 explicitly keeps the strict v2 path with no fallback (used by the pipeline's validate- staging step). V2_REPOSITORY_URL is the prod bucket constant used for the default repository value in _download_v2(); callers can still override it with --repository. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * feat(downloader): resolve hash-prefixed targets via N.targets.json The v2 TUF repository uses consistent-snapshot format: pointer files are stored as {sha256}.{version}.json on S3. Two changes to support this: 1. _make_updater now sets UpdaterConfig(prefix_targets_with_hash=True) so the TUF Updater resolves hash-prefixed paths automatically when calling download_target(). 2. get_pointer() now parses N.targets.json (after Updater.refresh()) to enumerate available versions for the project. This replaces the removed latest.json: when version=None, _resolve_version() scans all <project>/<ver>.json entries in targets metadata and returns the highest stable PEP 440 version. The disable_verification path fetches the metadata chain (timestamp → snapshot → targets) without verifying signatures to find the hash-prefixed URL, then fetches the pointer directly. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * feat(downloader): resolve latest via latest pointer target * Move v2 TUF root metadata * Simplify v2 downloader implementation * feat(downloader): add MissingVersion and MalformedPointerError exceptions Dedicated types replace the prior reuse of TargetNotFoundError for argument validation (which mislabeled the failure category) and the unchecked KeyError raised on a malformed pointer JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(downloader): harden v2 wheel fetch and pointer handling - Add explicit 60s timeout to urllib.request.urlopen so a stalled wheel fetch does not hang the Agent installer indefinitely. - Validate required pointer JSON keys (digest, length, wheel_path) and raise the new MalformedPointerError instead of an opaque KeyError. - Raise MissingVersion (a CLIError subclass) when --unsafe-disable-verification is set without --version, so the v1 fallback log reports the actual cause instead of "target not found". - Extract _verify_content to drop the pointer-is-None sentinel and make the verified and direct-download branches structurally parallel. - Add `from __future__ import annotations` so the PEP 604 unions stay compatible with the declared requires-python = ">=3.8". - Move logging.basicConfig out of the constructor and into the CLI entry point (separate commit); the class no longer mutates the root logger. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(downloader): make v2/v1 fallback handle validation errors and --force - Split _download_v2() into instantiate_v2_downloader() and run_v2_downloader() to mirror the v1 instantiate/run split and let the warning/validation branches be tested without patching sys.argv. - Re-raise user-input errors (CLIError, MissingVersion) before the broad except so they propagate as-is instead of triggering a spurious v1 retry and a misleading "v2 download failed" log line. - Add --force as a no-op compat stub on the v2 parser so v1-only callers do not trip parse_args -> SystemExit and silently skip the fallback. - Hoist `import logging` to module top (was lazy-imported in the except block) and own the verbose-to-level + logging.basicConfig setup that used to live inside TUFPointerDownloader.__init__. - Drop the meaningless `--v2 default=True` re-declaration; rename underscore-prefixed argparse dests to plain names. - Note in the fallback block that v1 offline tests now traverse v2 first on every invocation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(downloader): broaden v2 coverage and parametrize failure categories - Parametrize _v2_failure_category across all five (exc, category) cases and add DownloadError / TimeoutError coverage that the categorizer already handles but previous tests never asserted. - Replace direct calls to TUFPointerDownloader._target_path with a parametrized test that drives get_pointer and asserts on Updater.get_targetinfo so the behavior, not the private helper, is what's pinned. - Add failure-mode tests for malformed pointer JSON (one per required key), urllib HTTPError/URLError mid-download, and wheel_path without a leading slash so the URL-composition contract is visible. - Update test_direct_download_requires_explicit_version to expect MissingVersion now that argument-validation no longer reuses TargetNotFoundError. - Move @pytest.mark.offline from each class to a module-level pytestmark; drop the leading-underscore prefix on module constants to match AGENTS.md style. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style(downloader): sort test imports per project ruff config Ruff in CI uses the root ../pyproject.toml which treats datadog_checks as first-party. Reorder the test imports to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(downloader): address PR DataDog#23144 review feedback - exceptions.py: type-hint MalformedPointerError/DigestMismatch __init__; add LengthMismatch (split from the overloaded DigestMismatch). - download_v2.py: drop underscore from WHEEL_FETCH_TIMEOUT_SECONDS and REQUIRED_POINTER_KEYS per AGENTS.md; validate wheel_path leading slash via MalformedPointerError; verify length first (cheap early-out) before the sha256 digest check. - cli.py: add type hints on download(), _v2_parser(), instantiate_v2_downloader(), run_v2_downloader(); drop the unused _args parameter from run_v2_downloader; collapse the redundant (CLIError, MissingVersion) except clause to just CLIError. - test_v2_downloader.py: assert MalformedPointerError when wheel_path lacks a leading slash; split TestLengthMismatch from TestDigestMismatch; cover instantiate_v2_downloader validation/warning branches and the cli.download() v2-then-v1 fallback orchestration; drop the inline Updater patch in TestDisableVerification in favour of the fixture. * Fix v2 downloader blockers: narrow fallback, future import Narrow the v1 fallback in download() to a tuple of network/lookup errors. Previously every non-CLIError exception triggered v1 retry, including DigestMismatch / LengthMismatch / MalformedPointerError — i.e. integrity failures the v2 path is meant to surface were silently masked. Now those propagate; only TargetNotFoundError, DownloadError, TimeoutError, and urllib.error.URLError fall back. Add `from __future__ import annotations` to cli.py: the new module uses PEP 604 unions and PEP 585 subscripted generics at definition time, which crash on Python 3.8/3.9 (pyproject.toml declares requires-python = ">=3.8"). download_v2.py already had the import. Add parametrized test pinning the new behavior — DigestMismatch, LengthMismatch, and MalformedPointerError propagate without invoking the v1 downloader. Other review feedback (refactor download(), gate compat warnings on --v2, validate pointer field types, split download() into verified / direct, etc.) is deferred to a follow-up to keep this PR focused. * Preserve v1 downloader fallback behavior * Format v2 downloader tests * Add v2 downloader reviewer test coverage * Reuse v2 downloader test wheel name * Restore unsafe v1 fallback regression test --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3345d4d commit b5362c7

8 files changed

Lines changed: 822 additions & 4 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add v2 TUF pointer downloader support.

datadog_checks_downloader/datadog_checks/downloader/cli.py

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,30 @@
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44

5+
from __future__ import annotations
56

67
# 1st party.
78
import argparse
9+
import logging
810
import os
911
import re
1012
import sys
13+
import urllib.error
1114

1215
# 2nd party.
16+
from tuf.api.exceptions import DownloadError
17+
1318
from .download import DEFAULT_ROOT_LAYOUT_TYPE, REPOSITORY_URL_PREFIX, ROOT_LAYOUTS, TUFDownloader
14-
from .exceptions import NonCanonicalVersion, NonDatadogPackage
19+
from .download_v2 import V2_REPOSITORY_URL, TUFPointerDownloader
20+
from .exceptions import CLIError, MissingVersion, NonCanonicalVersion, NonDatadogPackage, TargetNotFoundError
21+
22+
V2_FALLBACK_ERRORS: tuple[type[BaseException], ...] = (
23+
MissingVersion,
24+
TargetNotFoundError,
25+
DownloadError,
26+
TimeoutError,
27+
urllib.error.URLError,
28+
)
1529

1630
# Private module functions.
1731

@@ -25,6 +39,14 @@ def __is_canonical(version):
2539
return re.match(P, version) is not None
2640

2741

42+
def _v2_failure_category(exc: Exception) -> str:
43+
if isinstance(exc, TargetNotFoundError):
44+
return 'target version not found'
45+
if isinstance(exc, (DownloadError, TimeoutError, urllib.error.URLError)):
46+
return 'network error'
47+
return 'other'
48+
49+
2850
def __find_shipped_integrations():
2951
# Recurse up from site-packages until we find the Agent root directory.
3052
# The relative path differs between operating systems.
@@ -142,6 +164,88 @@ def run_downloader(tuf_downloader, standard_distribution_name, version, ignore_p
142164
# Public module functions.
143165

144166

145-
def download():
146-
tuf_downloader, standard_distribution_name, version, ignore_python_version = instantiate_downloader()
147-
run_downloader(tuf_downloader, standard_distribution_name, version, ignore_python_version)
167+
def download() -> None:
168+
downloader, name, version, args = instantiate_v2_downloader()
169+
170+
if args.v2:
171+
warn_v2_ignored_args(args)
172+
run_v2_downloader(downloader, name, version)
173+
return
174+
175+
try:
176+
run_v2_downloader(downloader, name, version)
177+
except V2_FALLBACK_ERRORS as exc:
178+
# Integrity failures (DigestMismatch / LengthMismatch / MalformedPointerError) are
179+
# intentionally not in V2_FALLBACK_ERRORS — they must propagate, not be masked by v1.
180+
logging.getLogger(__name__).info(
181+
'v2 download failed (%s, %s: %s), falling back to v1',
182+
_v2_failure_category(exc),
183+
type(exc).__name__,
184+
exc,
185+
)
186+
run_downloader(*instantiate_downloader())
187+
except CLIError:
188+
# NonDatadogPackage and NonCanonicalVersion: v1 would raise the same.
189+
raise
190+
191+
192+
def _v2_parser() -> argparse.ArgumentParser:
193+
parser = argparse.ArgumentParser()
194+
195+
parser.add_argument(
196+
'standard_distribution_name',
197+
type=str,
198+
help='Standard distribution name of the desired Datadog check, e.g. datadog-postgres.',
199+
)
200+
parser.add_argument(
201+
'--repository', type=str, default=V2_REPOSITORY_URL, help='HTTPS base URL of the v2 TUF repository.'
202+
)
203+
parser.add_argument('--version', type=str, default=None, help='Version to download (default: latest stable).')
204+
parser.add_argument(
205+
'--unsafe-disable-verification',
206+
action='store_true',
207+
help='Disable TUF verification and wheel digest checks; requires --version and downloads /wheels directly.',
208+
)
209+
parser.add_argument('-v', '--verbose', action='count', default=0)
210+
parser.add_argument('--v2', action='store_true', default=False)
211+
212+
# v1 compat flags accepted as no-ops so callers upgrading from v1 get a warning, not an error.
213+
parser.add_argument('--type', type=str, default=None, dest='ignored_type')
214+
parser.add_argument('--ignore-python-version', action='store_true', dest='ignored_ignore_python_version')
215+
parser.add_argument('--force', action='store_true', dest='ignored_force')
216+
217+
return parser
218+
219+
220+
def warn_v2_ignored_args(args: argparse.Namespace) -> None:
221+
if args.ignored_type is not None:
222+
sys.stderr.write('WARNING: --type is not applicable with --v2 and will be ignored.\n')
223+
if args.ignored_ignore_python_version:
224+
sys.stderr.write(
225+
'NOTE: --ignore-python-version is not applicable with --v2 (wheel selection happens at publish time).\n'
226+
)
227+
228+
229+
def instantiate_v2_downloader() -> tuple[TUFPointerDownloader, str, str | None, argparse.Namespace]:
230+
args = _v2_parser().parse_args()
231+
232+
if not args.standard_distribution_name.startswith('datadog-'):
233+
raise NonDatadogPackage(args.standard_distribution_name)
234+
235+
if args.version and not __is_canonical(args.version):
236+
raise NonCanonicalVersion(args.version)
237+
238+
remainder = min(args.verbose, 5) % 6
239+
level = (6 - remainder) * 10
240+
logging.basicConfig(format='%(levelname)-8s: %(message)s', level=level)
241+
242+
downloader = TUFPointerDownloader(
243+
repository_url=args.repository,
244+
disable_verification=args.unsafe_disable_verification,
245+
)
246+
return downloader, args.standard_distribution_name, args.version, args
247+
248+
249+
def run_v2_downloader(downloader: TUFPointerDownloader, name: str, version: str | None) -> None:
250+
wheel_path = downloader.download(name, version=version)
251+
print(wheel_path) # pylint: disable=print-statement
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
{
2+
"signatures": [
3+
{
4+
"keyid": "ac5d650bc9aa17fdad54753fbf64e083f6f613286d0feef991ff61ec26874f2b",
5+
"sig": "3066023100beb16cde4c9e17c725713c6020cb5b11a65dd60a7b46f6842068815593e8f39cfa547ea89b6169474a8ee6a98c22f934023100f215434d25181f6f0d6a75a1bae4f09814678d3409cc0aaf0f8e1cb2b0a7bcb29acd5394b4df1751f7e73c641a17256b"
6+
},
7+
{
8+
"keyid": "6f0f52eb4cb14d590aafd5f7eb8d9a79477ac89794be2d3caade9fc39b3735e6",
9+
"sig": "306502306130792e890c5257cc8fc951e7c9a4009a5552affbd6bdf5cef3bac647de18f82918adbd4edfaa6d1624e2c378ee0ca4023100db6cff59e145b0113560116eac4466e52fc24e3ec3f02ee39fe7213718c2184d58e5473a5f29429b1a4c63e7ff87b83b"
10+
},
11+
{
12+
"keyid": "2d019dcc7a3e8da4d22bf364f0e0cb87937b2ced68339f3c53d305d1a9aadcce",
13+
"sig": "3066023100ed93223b4ab9784c00b73937cd431fe9d6af8906548124a10215ad93432523476a3265e712fe99b7555ea30ce5aeff30023100e55ae85f68da6ac322f912cae2a28facb6b3f014770d348a7c9daee100e5eb8ae78cc86321b20711f3b357d900a075a4"
14+
},
15+
{
16+
"keyid": "e942404daa3e8cb1143ab5f275df2f8c741ae002194147806bd6f05b8e2e816f",
17+
"sig": "3066023100a1a75a85dbe43db459e3d8c1bd935f2717bae0b1cba79ea5b9a5e785b7eb08cc30e08f96ba5fc0ccb9b9c97b9af456450231008dcc197a5de9a93649dfbd27e3f112321441913138c3377487ae85353a982cbffdc88029d681e432e86cfc14c51196ab"
18+
},
19+
{
20+
"keyid": "1286a08794005a5f1d679e56322f45fd3b55aa198f87bdc699f8213048602000",
21+
"sig": "3064023075188913725a1c2e9af59f8663b6a178156b64d87da126a5970a3b6a3399bdfe7f5c357099f2f1a4e83d52294551c41e0230080ef2ff77b7d558879cbe0eda409c3ba2fe080860506d4f2ede314374e39dc0b2bc466af51fdd258eb76171344d42af"
22+
},
23+
{
24+
"keyid": "65ccb05ff16285a3b65ea2db2581ed083bb19acfcbd130d5484c151baf28541f",
25+
"sig": "306602310086b9d6f39f795ad188223318f02e1d78b5798d34e333c1933e55891cc1b11cd6771b2d9ab1f5c1fc707d4815e3cc200d023100eb63f35f7cc2d0166357f2c209ecca63b82fc6bc9c310b9a0fa345957b1a0df102036ea6a6d3825d787eb7d3b3131e70"
26+
},
27+
{
28+
"keyid": "b59ade3245077bd622dc7bf41163a877e05272590cb4830632dc0d034717d735",
29+
"sig": "3066023100fa26ef91f1bb3cdf779cb6bbc43d70bab67a7c66103e61b8998698f469fad0d44002d4a9399ceac304b8ee1a8823fd99023100ecd415e58696ab4778f4bdf5187be3743ac372b29cd139111b3461a0da42f8f44e5bb3ec83c2bd0ce4b6281e585b8889"
30+
},
31+
{
32+
"keyid": "a07e905cad57b71374ef5e408d61936c31957b35026de0b8db3938878ccad637",
33+
"sig": "3066023100f4802957c21a0916677154494c4360260f5994c35c435d2bf2df39bc7cccca7fb437563d21ae128bcaa7909ead7d6e7802310097513f90e5e7dbe4bcb3f9b20308e966ec38960e8cad4869a4b32be8bd98726ded4a68c671d5f22858dd10ba3b56b04a"
34+
},
35+
{
36+
"keyid": "a442c20904f96e3a367e16037665bfb2e002bb2e9586cec4c96d83697a49fa2a",
37+
"sig": "30660231008d58d822ee1accd6bff07e79f171d61d122d35c1d51c86b2f2ada76cff695090fdf859127889f9a8d90e539277b5ab5a023100f0ad8d7ba6a25e27316a91bbc61c9b4d31f42c5c93662ea53d660af6b8ab9ee111b135b6844901ccd281fd9246d5f786"
38+
},
39+
{
40+
"keyid": "8969905969a712d54c9b327939aead62784587b54d1d03cbaa835f79205069bf",
41+
"sig": "3066023100894ecac9291e64ea6b84d168b886ef5829f4ad5b57c83b0ec745b644e4d19983e29c4681b5f744070a679e71fb4325af023100b6365d50a44e59932ea24d17a9fbc975cc7e7b44539d36dc7208470b1ddc9572b06266e149d1793a12a98cd62b803a95"
42+
}
43+
],
44+
"signed": {
45+
"_type": "root",
46+
"consistent_snapshot": true,
47+
"expires": "2027-04-21T15:31:05Z",
48+
"keys": {
49+
"1286a08794005a5f1d679e56322f45fd3b55aa198f87bdc699f8213048602000": {
50+
"keytype": "ecdsa",
51+
"keyval": {
52+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEruzzCikai9w8LqTLE4cxf0qRIFU6AQve\nnMmudDdNo22MCiOwbuYjJJ1dvRlMiSVrAGyv1+37h8aXGa5Qbx5nb4TEIRfaDth8\nhMbKJcQ7OOK/6SaltjNZh3VaZ396/WIC\n-----END PUBLIC KEY-----\n"
53+
},
54+
"scheme": "ecdsa-sha2-nistp384",
55+
"x-tuf-on-ci-keyowner": "@nouemankhal"
56+
},
57+
"2d019dcc7a3e8da4d22bf364f0e0cb87937b2ced68339f3c53d305d1a9aadcce": {
58+
"keytype": "ecdsa",
59+
"keyval": {
60+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAErfozI3wqaB8k6o6Mc7SPFiw8s1dLTaxk\nMmhMsdkk7QIl3t+gFzWNdXANEjN027g4S6Ty2CvdzovU37yD24td9pQBh8LGmfPa\nmU5cxtzRaXkCibibJrrvLxyyZTWZXW6C\n-----END PUBLIC KEY-----\n"
61+
},
62+
"scheme": "ecdsa-sha2-nistp384",
63+
"x-tuf-on-ci-keyowner": "@alexeypilyugin"
64+
},
65+
"4542ee95093cb434e0d80a4bb9dd9d96e6b67cda12759fa2648a7786f822e97d": {
66+
"keytype": "ecdsa",
67+
"keyval": {
68+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUV4g/gyxCdXKHK07QWO5z6S9lRhL88DO\nOb22g0dCOtxBB2sKojAUw3wXXz+SaUZRFgqfVvezbtsC4LSkkIlwA5MrJDA83kP2\nJRo4BQPtW8wZmtSvkkRQPSfAdXv975pg\n-----END PUBLIC KEY-----\n"
69+
},
70+
"scheme": "ecdsa-sha2-nistp384",
71+
"x-tuf-on-ci-online-uri": "awskms:arn:aws:kms:us-east-1:510233252802:key/9efe9e34-88f3-4ad3-8828-5340561e7c42"
72+
},
73+
"65ccb05ff16285a3b65ea2db2581ed083bb19acfcbd130d5484c151baf28541f": {
74+
"keytype": "ecdsa",
75+
"keyval": {
76+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEi44mg+tnJn41Cy4Lr42lQNRuZaHDY4d+\nB/oYkBRTiHl6n6hc6alGLS/1rWijAfSL7x7wgVeOrA5fp1ornW27vPOkRVWJO5Lv\nZcZXwJYi7svVFBkFjBAtAOF6DGuAEWc9\n-----END PUBLIC KEY-----\n"
77+
},
78+
"scheme": "ecdsa-sha2-nistp384",
79+
"x-tuf-on-ci-keyowner": "@lucia-sb"
80+
},
81+
"6f0f52eb4cb14d590aafd5f7eb8d9a79477ac89794be2d3caade9fc39b3735e6": {
82+
"keytype": "ecdsa",
83+
"keyval": {
84+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEAbWHH4rfNiJFz9gXLPV/QJK0tky4/nW1\nyMPnUe1GRac6UfGcjZvGA7mpmns4FYG1KuHbPhWlEDOQnLjiIiJkY2+Z96tywq6y\n+/e+0Gc2KSsVr0IAWALkTzQE+Q6ru+lj\n-----END PUBLIC KEY-----\n"
85+
},
86+
"scheme": "ecdsa-sha2-nistp384",
87+
"x-tuf-on-ci-keyowner": "@nubtron"
88+
},
89+
"8969905969a712d54c9b327939aead62784587b54d1d03cbaa835f79205069bf": {
90+
"keytype": "ecdsa",
91+
"keyval": {
92+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEkztjp5ixZrKt94qnSn4bisyEdgs0Wre/\nheazr1zx7MJUCLiHim0lEDWCB64m/YLru+W3/PLwTiQSavO62lB6y3ggjcq/ygwA\n5yxi0bP/MAJBZ0Hl+y+Q8BfKTZSrTb6j\n-----END PUBLIC KEY-----\n"
93+
},
94+
"scheme": "ecdsa-sha2-nistp384",
95+
"x-tuf-on-ci-keyowner": "@hadhemidd"
96+
},
97+
"a07e905cad57b71374ef5e408d61936c31957b35026de0b8db3938878ccad637": {
98+
"keytype": "ecdsa",
99+
"keyval": {
100+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEEilQwnno5GxJpoyxulKzkkHa0x0/ERDa\nf3m1ZCpF9SoT2B98T+BwT6noD+qlOwX7VKLFSQwl4/od53tu6Wt3s3P70zFviq+Y\n+chUOSCbA5y/TCvfwx4mLBruXI1QbVOh\n-----END PUBLIC KEY-----\n"
101+
},
102+
"scheme": "ecdsa-sha2-nistp384",
103+
"x-tuf-on-ci-keyowner": "@aarakke"
104+
},
105+
"a442c20904f96e3a367e16037665bfb2e002bb2e9586cec4c96d83697a49fa2a": {
106+
"keytype": "ecdsa",
107+
"keyval": {
108+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUJ7k4tiIZrWNLhrNrcBBMh4we3GiMlpo\ntwVy72lNw7aMxisK6ttP0mV30Yh1rX37DO6UUdeiWImrYBVfXFkP7z2QD9qKetny\nCeVHycA7uNby7yb7pljv2l2SpTgXACZk\n-----END PUBLIC KEY-----\n"
109+
},
110+
"scheme": "ecdsa-sha2-nistp384",
111+
"x-tuf-on-ci-keyowner": "@iliakur"
112+
},
113+
"ac5d650bc9aa17fdad54753fbf64e083f6f613286d0feef991ff61ec26874f2b": {
114+
"keytype": "ecdsa",
115+
"keyval": {
116+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEAWOvhm6nk7iY+EYK8ZnrxS49yqLf/ZTR\nJ74WY9Kz3ikjyXASkD4IgqJyyrmbMoqS9k6/RM/Zk6CAfPeZneDh1puVAlxy9nJD\nZp/OW78dVOqrlw1uQ0d+gfe7b4TcUNG4\n-----END PUBLIC KEY-----\n"
117+
},
118+
"scheme": "ecdsa-sha2-nistp384",
119+
"x-tuf-on-ci-keyowner": "@dkirov-dd"
120+
},
121+
"b59ade3245077bd622dc7bf41163a877e05272590cb4830632dc0d034717d735": {
122+
"keytype": "ecdsa",
123+
"keyval": {
124+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEd/9wooA4OKbC7hUO1OTZN3pnFbc85PDs\n+izKkDDSqj3yk8Pa39OJstT2BHvrn/B0BKMHhE6T/PN/rhorKVIVZ3UZErn1QCgG\nkkcFfA5MQm92SjIr9zAJea9bVUJhZ+PA\n-----END PUBLIC KEY-----\n"
125+
},
126+
"scheme": "ecdsa-sha2-nistp384",
127+
"x-tuf-on-ci-keyowner": "@sarah-witt"
128+
},
129+
"e942404daa3e8cb1143ab5f275df2f8c741ae002194147806bd6f05b8e2e816f": {
130+
"keytype": "ecdsa",
131+
"keyval": {
132+
"public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3VG/DJn/wmXh3bQ/LLjGMyKubQ1f5/1P\nJTVDYgTh5AC5zWxDSD26PoNpS29MecItPoM+pMy5YC99mwkEkxjNdwIke1Aons92\n8SVtL3BYH311oC6jLtFt+oqEunL5EdgJ\n-----END PUBLIC KEY-----\n"
133+
},
134+
"scheme": "ecdsa-sha2-nistp384",
135+
"x-tuf-on-ci-keyowner": "@kyle-neale"
136+
}
137+
},
138+
"roles": {
139+
"root": {
140+
"keyids": [
141+
"ac5d650bc9aa17fdad54753fbf64e083f6f613286d0feef991ff61ec26874f2b",
142+
"6f0f52eb4cb14d590aafd5f7eb8d9a79477ac89794be2d3caade9fc39b3735e6",
143+
"2d019dcc7a3e8da4d22bf364f0e0cb87937b2ced68339f3c53d305d1a9aadcce",
144+
"e942404daa3e8cb1143ab5f275df2f8c741ae002194147806bd6f05b8e2e816f",
145+
"1286a08794005a5f1d679e56322f45fd3b55aa198f87bdc699f8213048602000",
146+
"65ccb05ff16285a3b65ea2db2581ed083bb19acfcbd130d5484c151baf28541f",
147+
"b59ade3245077bd622dc7bf41163a877e05272590cb4830632dc0d034717d735",
148+
"a07e905cad57b71374ef5e408d61936c31957b35026de0b8db3938878ccad637",
149+
"a442c20904f96e3a367e16037665bfb2e002bb2e9586cec4c96d83697a49fa2a",
150+
"8969905969a712d54c9b327939aead62784587b54d1d03cbaa835f79205069bf"
151+
],
152+
"threshold": 2
153+
},
154+
"snapshot": {
155+
"keyids": [
156+
"4542ee95093cb434e0d80a4bb9dd9d96e6b67cda12759fa2648a7786f822e97d"
157+
],
158+
"threshold": 1,
159+
"x-tuf-on-ci-expiry-period": 365,
160+
"x-tuf-on-ci-signing-period": 60
161+
},
162+
"targets": {
163+
"keyids": [
164+
"ac5d650bc9aa17fdad54753fbf64e083f6f613286d0feef991ff61ec26874f2b",
165+
"6f0f52eb4cb14d590aafd5f7eb8d9a79477ac89794be2d3caade9fc39b3735e6",
166+
"2d019dcc7a3e8da4d22bf364f0e0cb87937b2ced68339f3c53d305d1a9aadcce",
167+
"e942404daa3e8cb1143ab5f275df2f8c741ae002194147806bd6f05b8e2e816f",
168+
"1286a08794005a5f1d679e56322f45fd3b55aa198f87bdc699f8213048602000",
169+
"65ccb05ff16285a3b65ea2db2581ed083bb19acfcbd130d5484c151baf28541f",
170+
"b59ade3245077bd622dc7bf41163a877e05272590cb4830632dc0d034717d735",
171+
"a07e905cad57b71374ef5e408d61936c31957b35026de0b8db3938878ccad637",
172+
"a442c20904f96e3a367e16037665bfb2e002bb2e9586cec4c96d83697a49fa2a",
173+
"8969905969a712d54c9b327939aead62784587b54d1d03cbaa835f79205069bf"
174+
],
175+
"threshold": 1
176+
},
177+
"timestamp": {
178+
"keyids": [
179+
"4542ee95093cb434e0d80a4bb9dd9d96e6b67cda12759fa2648a7786f822e97d"
180+
],
181+
"threshold": 1,
182+
"x-tuf-on-ci-expiry-period-hours": 48,
183+
"x-tuf-on-ci-signing-period-hours": 24
184+
}
185+
},
186+
"spec_version": "1.0.31",
187+
"version": 1,
188+
"x-tuf-on-ci-expiry-period": 365,
189+
"x-tuf-on-ci-signing-period": 60
190+
}
191+
}

0 commit comments

Comments
 (0)