Skip to content

Conversation

@dkirov-dd
Copy link
Contributor

Summary

Implements a proof-of-concept for manually uploading integration wheels to S3 with TUF (The Update Framework) signing support, enabling secure distribution with cryptographic verification.

Key Features

  • Enhanced Build Command: Generates pointer files with complete metadata (name, version, URI, digest, length, custom fields)
  • Smart Upload Command:
    • Organizes files in S3 with proper structure (simple/, pointers/, metadata/)
    • Generates PEP 503 compliant indexes using constant-complexity approach (no wheel downloads)
    • Idempotent uploads skip re-uploading identical versions
  • New Sign Command: Generates and signs TUF metadata (root, targets, snapshot, timestamp) with Ed25519 keys
  • AWS-Vault Integration: Automatically handles authentication without verbose wrapper commands
  • Integration Test Script: End-to-end workflow verification

Architecture

S3 Bucket Structure:

test-public-integration-wheels/
├── simple/
│   ├── index.html (root index)
│   └── datadog-{integration}/
│       ├── index.html (package index)
│       └── *.whl
├── pointers/
│   └── datadog-{integration}/
│       └── *.pointer
└── metadata/
    ├── root.json
    ├── targets.json
    ├── snapshot.json
    └── timestamp.json

Pointer File Format (YAML):

pointer:
  name: datadog-postgres
  version: 1.2.3
  uri: https://test-public-integration-wheels.s3.eu-north-1.amazonaws.com/simple/datadog-postgres/datadog_postgres-1.2.3-py3-none-any.whl
  digest: abc123...  # SHA256
  length: 12345      # Bytes
  custom: {}         # Reserved for in-toto

Usage

Before (verbose):

aws-vault exec sso-agent-integrations-dev-account-admin -- ddev release upload --public aws_neuron

After (simple):

ddev release build postgres
ddev release upload --public postgres  # Auto aws-vault!
ddev release sign --generate-keys      # Auto aws-vault!

Integration Test:

./scripts/test_tuf_workflow.sh postgres

Files Added

  • datadog_checks_dev/datadog_checks/dev/tooling/simple_index.py (215 lines): PEP 503 index generation
  • datadog_checks_dev/datadog_checks/dev/tooling/tuf_signing.py (393 lines): TUF metadata generation/signing
  • datadog_checks_dev/datadog_checks/dev/tooling/commands/release/sign.py (119 lines): Sign command implementation
  • datadog_checks_dev/datadog_checks/dev/tooling/aws_helpers.py (113 lines): AWS-vault integration helpers
  • scripts/test_tuf_workflow.sh: End-to-end integration test script

Files Modified

  • datadog_checks_dev/datadog_checks/dev/tooling/release.py: Enhanced pointer generation and upload logic
  • datadog_checks_dev/datadog_checks/dev/tooling/commands/release/upload.py: Added aws-vault integration
  • ddev/src/ddev/cli/release/__init__.py: Registered sign command

POC Limitations

  • Dummy keys only: Uses auto-generated Ed25519 keys (not production-ready)
  • No in-toto integration: Supply chain verification placeholder only
  • Manual signing: ddev release sign must be run manually after uploads
  • checks_downloader not modified: Out of scope for this POC

Future Work

  • Integrate with checks_downloader to consume pointer files
  • Implement production key management (HSM/KMS)
  • Add in-toto supply chain verification
  • Automate signing in CI/CD pipeline
  • Add CDN for metadata distribution

Test Plan

  • Build integration creates pointer file with all required fields
  • Upload organizes files in proper S3 structure
  • Upload generates PEP 503 compliant indexes without downloading wheels
  • Sign command generates valid TUF metadata
  • TUF metadata properly signed with Ed25519 keys
  • AWS-vault integration works seamlessly
  • Integration test script validates end-to-end workflow

🤖 Generated with Claude Code

dkirov-dd and others added 2 commits December 4, 2025 11:21
Implement a proof-of-concept system for manually uploading integration wheels
to S3 with TUF (The Update Framework) signing support. This enables secure
distribution of integration wheels with cryptographic verification.

Key features:
- Enhanced build command generates pointer files with package metadata (name,
  version, URI, digest, length)
- Upload command organizes files in S3 (simple/, pointers/, metadata/) and
  generates PEP 503 compliant indexes using constant-complexity approach
- New sign command generates and signs TUF metadata (root, targets, snapshot,
  timestamp) with Ed25519 keys
- AWS-vault integration automatically handles authentication without verbose
  wrapper commands
- Idempotent uploads skip re-uploading identical versions

Architecture:
- Pointer files stored in integration dist/ folders alongside wheels
- S3 bucket: test-public-integration-wheels (eu-north-1)
- Simple indexes generated via dumb-pypi approach (no wheel downloads needed)
- TUF metadata with dummy keys for POC (production requires HSM/KMS)

Usage:
  ddev release build <integration>
  ddev release upload --public <integration>
  ddev release sign --generate-keys

Files added:
- datadog_checks_dev/datadog_checks/dev/tooling/simple_index.py: PEP 503 index generation
- datadog_checks_dev/datadog_checks/dev/tooling/tuf_signing.py: TUF metadata generation/signing
- datadog_checks_dev/datadog_checks/dev/tooling/commands/release/sign.py: Sign command
- datadog_checks_dev/datadog_checks/dev/tooling/aws_helpers.py: AWS-vault integration
- scripts/test_tuf_workflow.sh: End-to-end integration test script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@github-actions
Copy link

github-actions bot commented Dec 4, 2025

⚠️ Recommendation: Add qa/skip-qa label

This PR does not modify any files shipped with the agent.

To help streamline the release process, please consider adding the qa/skip-qa label if these changes do not require QA testing.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 179 to 183
URI_TEMPLATE = "https://test-public-integration-wheels.s3.eu-north-1.amazonaws.com/simple/{}/{}"
package_name = os.path.basename(package_path)
wheel_name = os.path.basename(wheel_path)
version = wheel_name.split("-")[1]
uri = URI_TEMPLATE.format(package_name, wheel_name)

Choose a reason for hiding this comment

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

P1 Badge Generate pointer URIs with wrong package prefix

Pointer creation uses the folder name (package_path basename) when building the pointer URI, so it writes links like .../simple/postgres/<wheel> even though upload_package uploads wheels under simple/datadog-postgres/ via get_package_name. As a result every generated pointer file references a path that doesn’t exist in S3, so downstream consumers following the pointer/TUF metadata won’t be able to fetch the wheel.

Useful? React with 👍 / 👎.

Comment on lines 196 to 200
pointer_content = yaml.safe_load(response['Body'])
pointer = pointer_content.get('pointer', {})

# Calculate hash of pointer file itself
response['Body'].seek(0)

Choose a reason for hiding this comment

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

P1 Badge Signing skips targets due to non-seekable S3 body

In generate_targets_metadata the pointer content is parsed directly from the S3 StreamingBody and then the code calls response['Body'].seek(0) to re-read it for hashing. StreamingBody objects are not seekable, so the seek raises and is caught by the broad exception handler, causing each pointer to be skipped and leaving targets metadata empty while the command still reports success.

Useful? React with 👍 / 👎.

dkirov-dd and others added 3 commits December 4, 2025 18:53
Address two P1 issues identified in PR review:

1. Fix pointer URI generation to use correct package name
   - Previously used folder name (e.g., "postgres") instead of package name
     (e.g., "datadog-postgres") when building pointer URIs
   - This caused pointers to reference non-existent S3 paths
   - Now uses get_package_name() to get the correct "datadog-*" prefix
   - Pointer URIs now correctly match the S3 upload paths

2. Fix S3 StreamingBody seek issue in TUF signing
   - S3 StreamingBody objects are not seekable, causing seek(0) to fail
   - This caused all pointer files to be silently skipped during signing
   - Now reads the body once into bytes, then uses those bytes for both
     YAML parsing and hash calculation
   - Targets metadata now correctly includes all pointer files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Switch from PyNaCl (libsodium bindings) to the standard cryptography
library for Ed25519 key generation. The cryptography library is more
widely used, better maintained, and is the de facto standard for
cryptographic operations in Python.

Changes:
- Added _generate_ed25519_key_with_cryptography() helper function
- Uses cryptography.hazmat.primitives.asymmetric.ed25519 for key generation
- Converts keys to securesystemslib format for compatibility with signing
- No additional dependencies needed (cryptography is already included via
  securesystemslib[crypto])

This fixes the error: "ed25519 key support requires the nacl library"

Tested: Key generation works successfully and produces valid Ed25519 keys

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Complete the migration from PyNaCl to cryptography by replacing the
signing function. The previous commit only replaced key generation,
but securesystemslib.keys.create_signature() also requires PyNaCl.

Changes:
- Replaced securesystemslib.keys.create_signature() with custom
  implementation using cryptography.hazmat.primitives.asymmetric.ed25519
- Signing now uses Ed25519PrivateKey.sign() from cryptography library
- Signature format remains compatible with securesystemslib/TUF
- No PyNaCl dependency required anywhere in the signing workflow

This completely fixes the error: "ed25519 key support requires the nacl library"

Tested: Complete workflow (key generation + signing) works successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@dkirov-dd dkirov-dd changed the title Add POC for manual integration wheel upload with TUF signing [POC] Manual integration release and signing Dec 4, 2025
@codecov
Copy link

codecov bot commented Dec 4, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.15%. Comparing base (5cf5dc7) to head (2b3e4f5).
⚠️ Report is 48 commits behind head on master.

Additional details and impacted files
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Enable the checks_downloader to download from the new POC S3 bucket
(test-public-integration-wheels) with TUF-signed pointer files.

Changes:
- Added REPOSITORY_URL_PREFIX_POC constant for POC S3 bucket URL
- Added --use-poc CLI flag to switch to POC repository
- Updated TUFDownloader to handle different metadata paths:
  - POC uses 'metadata/' instead of 'metadata.staged/'
  - POC uses root path instead of 'targets/' prefix
- Pass use_poc parameter through CLI to TUFDownloader

Usage:
  # Download from production (default)
  python -m datadog_checks.downloader datadog-postgres

  # Download from POC S3 bucket
  python -m datadog_checks.downloader datadog-postgres --use-poc

The downloader will:
1. Connect to test-public-integration-wheels.s3.eu-north-1.amazonaws.com
2. Download TUF metadata from metadata/ prefix
3. Download wheels from simple/{package}/ paths
4. Verify integrity using TUF signatures

Note: This enables testing the complete POC workflow:
  ddev release build postgres
  ddev release upload --public postgres
  ddev release sign --generate-keys
  python -m datadog_checks.downloader datadog-postgres --use-poc

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
dkirov-dd and others added 2 commits December 5, 2025 15:09
The downloader now properly implements the pointer-based architecture:
- Downloads pointer files via TUF (which verifies cryptographic integrity)
- Parses pointer to extract wheel URI and expected SHA256 digest
- Downloads wheel directly from S3 using the URI
- Verifies wheel digest matches the trusted pointer

This fixes the issue where the downloader was trying to download wheels
directly through TUF, but TUF metadata only tracks pointer files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Adds boto3 as a dependency and updates the wheel download flow to use
authenticated S3 access:
- Parses S3 URI to extract bucket, key, and region
- Uses boto3 client with AWS credentials from standard credential chain
- Downloads wheel with authentication support
- Verifies digest after download

This enables the downloader to work with non-public S3 buckets that
require authentication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
s3_key = parsed.path.lstrip('/')

# Extract region from hostname if present
if '.s3.' in parsed.hostname and '.amazonaws.com' in parsed.hostname:

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

The string
.amazonaws.com
may be at an arbitrary position in the sanitized URL.

Copilot Autofix

AI 3 days ago

To fix the problem, replace the substring checks in line 209 with a robust, structural check of the hostname. Specifically, parse parsed.hostname into its components, match the format [bucket_name].s3.[region].amazonaws.com, and ensure that the domain truly ends with .amazonaws.com, not that .amazonaws.com is simply present somewhere in the hostname. Use urllib.parse for URL parsing and then compare the domain suffix directly (e.g., hostname.endswith('.amazonaws.com')) and verify the presence of the S3 marker at the correct position. Consider using regular expressions to match the precise pattern required for S3 endpoints.

The code to change is in the block starting line 209:

209:             if '.s3.' in parsed.hostname and '.amazonaws.com' in parsed.hostname:
210:                 region = parsed.hostname.split('.s3.')[1].split('.amazonaws.com')[0]
211:             else:
212:                 region = None

This should be replaced by a robust check (likely using a regular expression) that matches exactly the S3 endpoint structure. For example, extract bucket, region, and ensure the domain ends with .amazonaws.com.

No new third-party dependencies are needed; only standard library modules (re) should be used if necessary.

Suggested changeset 1
datadog_checks_downloader/datadog_checks/downloader/download.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/datadog_checks_downloader/datadog_checks/downloader/download.py b/datadog_checks_downloader/datadog_checks/downloader/download.py
--- a/datadog_checks_downloader/datadog_checks/downloader/download.py
+++ b/datadog_checks_downloader/datadog_checks/downloader/download.py
@@ -206,8 +206,11 @@
             s3_key = parsed.path.lstrip('/')
 
             # Extract region from hostname if present
-            if '.s3.' in parsed.hostname and '.amazonaws.com' in parsed.hostname:
-                region = parsed.hostname.split('.s3.')[1].split('.amazonaws.com')[0]
+            # Ensure hostname matches pattern: <bucket>.s3.<region>.amazonaws.com
+            s3_pattern = r'^(?P<bucket>[a-zA-Z0-9-_.]+)\.s3\.(?P<region>[a-z0-9-]+)\.amazonaws\.com$'
+            match = re.match(s3_pattern, parsed.hostname or "")
+            if match:
+                region = match.group('region')
             else:
                 region = None
 
EOF
@@ -206,8 +206,11 @@
s3_key = parsed.path.lstrip('/')

# Extract region from hostname if present
if '.s3.' in parsed.hostname and '.amazonaws.com' in parsed.hostname:
region = parsed.hostname.split('.s3.')[1].split('.amazonaws.com')[0]
# Ensure hostname matches pattern: <bucket>.s3.<region>.amazonaws.com
s3_pattern = r'^(?P<bucket>[a-zA-Z0-9-_.]+)\.s3\.(?P<region>[a-z0-9-]+)\.amazonaws\.com$'
match = re.match(s3_pattern, parsed.hostname or "")
if match:
region = match.group('region')
else:
region = None

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
dkirov-dd and others added 9 commits December 5, 2025 15:27
…cation)

Since this is a POC branch, simplified the downloader to only support
the new pointer-based architecture:

- Removed --use-poc flag (always uses POC mode now)
- Changed default repository to test-public-integration-wheels bucket
- Removed all in-toto verification code (~130 lines)
- Removed related imports (glob, tempfile, shutil, in_toto libraries)
- Simplified download flow to only use pointer-based approach
- Updated CLI help text to remove in-toto references

The downloader now has a single, clean flow:
1. Download pointer file via TUF
2. Parse pointer to get wheel URI and digest
3. Download wheel with boto3 (authenticated)
4. Verify wheel digest

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
TUF metadata files (root.json, targets.json, snapshot.json, timestamp.json)
and simple package indexes need to be publicly accessible since:
- TUF updater doesn't use AWS credentials when downloading metadata
- Package indexes are accessed by pip/pip-like tools

Added ACL='public-read' to S3 uploads for:
- All TUF metadata files in tuf_signing.py
- Package and root index.html files in simple_index.py

Note: Pointer files and wheel files remain private and require AWS
authentication to download (controlled by boto3 credential chain).

This fixes the 403 Forbidden error when downloading TUF metadata.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…oader

Since this is a POC, simplified the architecture by making all files
publicly readable:
- Wheels are now uploaded with ACL='public-read'
- Pointer files are now uploaded with ACL='public-read'
- Downloader uses urllib.request.urlopen() instead of boto3
- Removed boto3 dependency from downloader

This eliminates the need for AWS authentication in the downloader while
maintaining security through TUF's cryptographic verification. The
pointer files contain the expected wheel digest, and TUF ensures the
pointer files haven't been tampered with.

Security model:
- TUF metadata: Public (cryptographically signed)
- Pointer files: Public (tracked and verified by TUF)
- Wheel files: Public (digest verified against pointer)
- Simple indexes: Public (standard for PyPI-compatible repos)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Since S3 bucket cannot be made public, reverted to authenticated downloads:

Upload changes (release.py):
- Pointer files: public-read (for TUF access)
- Wheel files: private (requires AWS authentication)

Downloader changes:
- Added boto3 back for authenticated S3 downloads
- Added inline aws-vault integration to CLI
- Downloads wheels using boto3.client('s3').get_object()
- Verifies wheel digest after download

CLI integration:
- Added --aws-vault-profile option
- Auto-detects AWS credentials
- Re-execs with aws-vault if credentials missing
- Uses default profile: sso-agent-integrations-dev-account-admin

Usage:
  # With explicit profile
  datadog-checks-downloader datadog-postgres --aws-vault-profile my-profile

  # Auto-detect (will use aws-vault if no credentials)
  datadog-checks-downloader datadog-postgres

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed automatic authentication handling - users will manage AWS
credentials themselves from the command line using aws-vault or other
methods.

Changes:
- Removed __check_aws_credentials() helper
- Removed __exec_with_aws_vault() helper
- Removed __ensure_aws_credentials() helper
- Removed --aws-vault-profile CLI option
- Removed subprocess import (no longer needed)

boto3 still present and will use whatever credentials are available
in the environment (AWS_ACCESS_KEY_ID, aws-vault, AWS CLI config, etc.)

Usage:
  # With aws-vault
  aws-vault exec profile -- datadog-checks-downloader datadog-postgres

  # With AWS CLI configured
  datadog-checks-downloader datadog-postgres

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…TUF version discovery

- Standardize pointer filename format to use full package name (datadog-postgres-23.2.0.pointer)
- Add MinIO container management for local S3 development and testing
- Implement get_s3_client() helper to switch between AWS S3 and local MinIO
- Add --local flag to upload and sign commands for local development workflow
- Refactor downloader to discover versions from TUF pointer files instead of simple index HTML
- Add __get_versions_from_pointers() method to extract versions from TUF targets metadata
- Store pointer path during version discovery for use in download phase
- Maintain backward compatibility with simple index when TUF is disabled
- Add LOCAL_DEVELOPMENT.md documentation for local testing workflow
- Add bucket-policy.json and make_bucket_public.sh for MinIO configuration
- Migrate TUF signing from legacy python-tuf to new Metadata API

Rationale: The previous implementation required simple index HTML files to be included in TUF targets metadata for version discovery, but only pointer files were actually signed. This caused "TargetNotFoundError" when trying to download packages. The new pointer-based discovery eliminates this dependency by extracting versions directly from pointer filenames in TUF targets. The local MinIO infrastructure enables developers to test the complete upload, signing, and download workflow without AWS credentials, significantly improving development velocity and making it easier to iterate on TUF-related features.
- Replace root.json with keys generated for local MinIO testing
- Update key IDs and signatures to match current TUF metadata
- Set consistent_snapshot to false (matching local development configuration)
- Update expiration date to 2026-12-08

Rationale: The root.json needs to match the keys used to sign the TUF metadata in MinIO for local development. The previous root.json contained different keys, causing "timestamp was signed by 0/1 keys" errors during downloads. This update synchronizes the trusted root keys with the actual signing keys generated by `ddev release sign --local`.
- Store repository_url_prefix as instance variable for URI rewriting
- Detect localhost URLs and rewrite production S3 URIs to use local MinIO endpoint
- Configure boto3 client with MinIO credentials for localhost URLs
- Parse local MinIO URI format (http://localhost:9000/bucket/path) correctly
- Maintain AWS S3 behavior for production URLs

Rationale: When using local MinIO for development, pointer files contain production S3 URIs (generated during build), but wheels only exist in the local MinIO instance. The downloader needs to transparently rewrite these URIs to use the local endpoint when the repository URL points to localhost. This enables end-to-end local testing of the TUF download workflow without requiring AWS credentials or uploading test wheels to production S3.
…elopment

Automatically clear cached TUF metadata files (timestamp.json, snapshot.json,
targets.json) when using localhost repository URLs. This eliminates the need
for manual cache deletion after running 'ddev release sign --local'.

The downloader now detects localhost URLs and clears stale metadata before
initializing the TUF Updater, ensuring fresh metadata is always fetched in
local development while preserving production caching behavior.

Changes:
- Add _clear_metadata_cache() method to TUFDownloader
- Detect localhost URLs in __init__() and trigger cache clearing
- Update LOCAL_DEVELOPMENT.md to document automatic behavior
- Update root.json with refreshed local development keys
- Apply code formatting improvements to release.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants