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/&lt;release_key&gt;</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 }} &lt;{{ result.email 
}}&gt;</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

Reply via email to