This is an automated email from the ASF dual-hosted git repository. sbp pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tooling-atr-experiments.git
The following commit(s) were added to refs/heads/main by this push: new aca72b6 Verify all release package signatures aca72b6 is described below commit aca72b6e36f47de2b76adb2eead7afe54629b6ae Author: Sean B. Palmer <s...@miscoranda.com> AuthorDate: Fri Feb 14 17:01:50 2025 +0200 Verify all release package signatures --- atr/routes.py | 100 ++++++++++++++++- atr/templates/pages.html | 8 ++ atr/templates/release-signature-verify.html | 164 ++++++++++++++++++++++++++++ atr/templates/user-uploads.html | 3 + 4 files changed, 274 insertions(+), 1 deletion(-) diff --git a/atr/routes.py b/atr/routes.py index f65a69d..8721dbf 100644 --- a/atr/routes.py +++ b/atr/routes.py @@ -18,10 +18,11 @@ "routes.py" import hashlib +from io import BufferedReader import json import pprint from pathlib import Path -from typing import List, Tuple, Optional +from typing import List, Tuple, Optional, Dict, Any import datetime import asyncio @@ -317,6 +318,47 @@ async def root_database_debug() -> str: return f"Database using {current_app.config['DATA_MODELS_FILE']} has {len(pmcs)} PMCs" +@APP.route("/release/signatures/verify/<release_key>") +@require(R.committer) +async def root_release_signatures_verify(release_key: str) -> str: + """Verify the GPG signatures for all packages in a release candidate.""" + session = await session_read() + if session is None: + raise ASFQuartException("Not authenticated", errorcode=401) + + with Session(current_app.config["engine"]) as db_session: + # Get the release and its packages + statement = select(Release).where(Release.storage_key == release_key) + release = db_session.exec(statement).first() + if not release: + raise ASFQuartException("Release not found", errorcode=404) + + # Verify each package's signature + verification_results = [] + storage_dir = Path(current_app.config["RELEASE_STORAGE_DIR"]) + + for package in release.packages: + result = {"file": package.file} + + artifact_path = storage_dir / package.file + signature_path = storage_dir / package.signature + + if not artifact_path.exists(): + result["error"] = "Package artifact file not found" + elif not signature_path.exists(): + result["error"] = "Package signature file not found" + else: + # Verify the signature + result = await verify_gpg_signature(artifact_path, signature_path) + result["file"] = package.file + + verification_results.append(result) + + return await render_template( + "release-signature-verify.html", release=release, verification_results=verification_results + ) + + @APP.route("/pages") async def root_pages() -> str: "List all pages on the website." @@ -600,3 +642,59 @@ async def user_keys_add_session( "data": pprint.pformat(key), }, ) + + +async def verify_gpg_signature(artifact_path: Path, signature_path: Path) -> Dict[str, Any]: + """ + Verify a GPG signature for a release artifact. + Returns a dictionary with verification results and debug information. + """ + gpg = gnupg.GPG() + try: + with open(signature_path, "rb") as sig_file: + return await verify_gpg_signature_file(gpg, sig_file, artifact_path) + except Exception as e: + return { + "verified": False, + "error": str(e), + "status": "Verification failed", + "debug_info": {"exception_type": type(e).__name__, "exception_message": str(e)}, + } + + +async def verify_gpg_signature_file(gpg: gnupg.GPG, sig_file: BufferedReader, artifact_path: Path) -> Dict[str, Any]: + # Run the blocking GPG verification in a thread + verified = await asyncio.to_thread(gpg.verify_file, sig_file, str(artifact_path)) + + # Collect all available information for debugging + debug_info = { + "key_id": verified.key_id or "Not available", + "fingerprint": verified.fingerprint or "Not available", + "pubkey_fingerprint": verified.pubkey_fingerprint or "Not available", + "creation_date": verified.creation_date or "Not available", + "timestamp": verified.timestamp or "Not available", + "username": verified.username or "Not available", + "status": verified.status or "Not available", + "valid": bool(verified), + "trust_level": verified.trust_level if hasattr(verified, "trust_level") else "Not available", + "trust_text": verified.trust_text if hasattr(verified, "trust_text") else "Not available", + "stderr": verified.stderr if hasattr(verified, "stderr") else "Not available", + } + + if not verified: + return { + "verified": False, + "error": "No valid signature found", + "status": "Invalid signature", + "debug_info": debug_info, + } + + return { + "verified": True, + "key_id": verified.key_id, + "timestamp": verified.timestamp, + "username": verified.username or "Unknown", + "email": verified.pubkey_fingerprint or "Unknown", + "status": "Valid signature", + "debug_info": debug_info, + } diff --git a/atr/templates/pages.html b/atr/templates/pages.html index b018e11..a627b68 100644 --- a/atr/templates/pages.html +++ b/atr/templates/pages.html @@ -160,6 +160,14 @@ Access: <span class="access-requirement committer">Committer</span> </div> </div> + + <div class="endpoint"> + <h3>/release/signatures/verify/<release_key></h3> + <div class="endpoint-description">Verify GPG signatures for all packages in a release candidate.</div> + <div class="endpoint-meta"> + Access: <span class="access-requirement committer">Committer</span> + </div> + </div> </div> <div class="endpoint-group"> diff --git a/atr/templates/release-signature-verify.html b/atr/templates/release-signature-verify.html new file mode 100644 index 0000000..39e895c --- /dev/null +++ b/atr/templates/release-signature-verify.html @@ -0,0 +1,164 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1.0" /> + <meta name="description" content="Verify release candidate signatures." /> + <title>ATR | Verify Release Signatures</title> + <link rel="stylesheet" href="{{ url_for('static', filename='root.css') }}" /> + <style> + .release-info { + margin-bottom: 2rem; + } + + .package-list { + margin: 1rem 0; + } + + .package { + border: 1px solid #ddd; + padding: 1rem; + margin: 1rem 0; + border-radius: 4px; + } + + .package-info { + margin-bottom: 1rem; + } + + .verification-status { + margin-top: 1rem; + padding: 1rem; + border-radius: 4px; + background: #f5f5f5; + } + + .navigation { + margin-top: 2rem; + } + + .navigation a { + margin-right: 1rem; + } + + .error { + color: #dc3545; + font-weight: bold; + } + + .status.success { + color: #28a745; + } + + .status.failure { + color: #dc3545; + } + + .signature-details { + margin-top: 1rem; + padding: 1rem; + border-radius: 4px; + background: #f5f5f5; + } + + .debug-info { + margin-top: 1rem; + padding: 1rem; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + } + + .debug-info h3 { + margin-top: 0; + color: #666; + } + + .debug-info dl { + margin: 0; + display: grid; + grid-template-columns: auto 1fr; + gap: 0.5rem 1rem; + } + + .debug-info dt { + font-weight: bold; + color: #666; + } + + .debug-info dd { + margin: 0; + word-break: break-all; + } + + pre.stderr { + background: #f8f9fa; + padding: 0.5rem; + border-radius: 2px; + overflow-x: auto; + margin: 0.5rem 0; + white-space: pre-wrap; + } + </style> + </head> + <body> + <h1>Verify Release Signatures</h1> + + <div class="release-info"> + <h2>{{ release.pmc.project_name }}</h2> + <p> + Stage: {{ release.stage.value }} + • + Phase: {{ release.phase.value }} + </p> + </div> + + <div class="package-list"> + {% for result in verification_results %} + <div class="package"> + <div class="package-info"> + <div>File: {{ result.file }}</div> + </div> + + <div class="verification-status"> + {% if result.error %} + <p class="error">Error: {{ result.error }}</p> + {% else %} + <p class="status {% if result.verified %}success{% else %}failure{% endif %}">Status: {{ result.status }}</p> + {% if result.verified %} + <div class="signature-details"> + <p>Key ID: {{ result.key_id }}</p> + <p>Signed by: {{ result.username }} <{{ result.email }}></p> + <p>Timestamp: {{ result.timestamp }}</p> + </div> + {% endif %} + {% endif %} + + {% if result.debug_info %} + <div class="debug-info"> + <h3>Debug Information</h3> + <dl> + {% for key, value in result.debug_info.items() %} + <dt>{{ key }}</dt> + <dd> + {% if key == 'stderr' and value != 'Not available' %} + <pre class="stderr">{{ value }}</pre> + {% else %} + {{ value }} + {% endif %} + </dd> + {% endfor %} + </dl> + </div> + {% endif %} + </div> + </div> + {% endfor %} + </div> + + <div class="navigation"> + <a href="{{ url_for('root_user_uploads') }}">Back to Your Uploads</a> + <a href="{{ url_for('root_pages') }}">Return to Main Page</a> + </div> + </body> +</html> diff --git a/atr/templates/user-uploads.html b/atr/templates/user-uploads.html index c4dd822..363b2db 100644 --- a/atr/templates/user-uploads.html +++ b/atr/templates/user-uploads.html @@ -69,6 +69,9 @@ <div>File: {{ package.file }}</div> <div>Signature: {{ package.signature }}</div> <div>Checksum (SHA-512): {{ package.checksum }}</div> + <p class="package-actions"> + <a href="{{ url_for('root_release_signatures_verify', release_key=release.storage_key) }}">Verify Signatures</a> + </p> </div> {% endfor %} </div> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tooling.apache.org For additional commands, e-mail: dev-h...@tooling.apache.org