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-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 4694499  Use standard SSH key fingerprint calculation and add a 
browser test
4694499 is described below

commit 46944998b9d447a00a75597a5efd8a869a0150c0
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Apr 14 17:25:50 2025 +0100

    Use standard SSH key fingerprint calculation and add a browser test
---
 atr/routes/keys.py | 25 +++++++++----------
 playwright/test.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 82 insertions(+), 15 deletions(-)

diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 5d2010f..6c4156a 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -19,6 +19,7 @@
 
 import asyncio
 import base64
+import binascii
 import contextlib
 import datetime
 import hashlib
@@ -29,7 +30,6 @@ import re
 from collections.abc import AsyncGenerator, Sequence
 
 import asfquart as asfquart
-import cryptography.hazmat.primitives.serialization as serialization
 import gnupg
 import quart
 import werkzeug.datastructures as datastructures
@@ -89,24 +89,21 @@ def key_ssh_fingerprint(ssh_key_string: str) -> str:
     # I.e. TYPE DATA COMMENT
     ssh_key_parts = ssh_key_string.strip().split()
     if len(ssh_key_parts) >= 2:
-        key_type = ssh_key_parts[0]
+        # We discard the type, which is ssh_key_parts[0]
         key_data = ssh_key_parts[1]
         # We discard the comment, which is ssh_key_parts[2]
 
-        # Parse the key
-        key = serialization.load_ssh_public_key(f"{key_type} 
{key_data}".encode())
-
-        # Get raw public key bytes
-        public_bytes = key.public_bytes(
-            encoding=serialization.Encoding.DER, 
format=serialization.PublicFormat.SubjectPublicKeyInfo
-        )
+        # Standard fingerprint calculation
+        try:
+            decoded_key_data = base64.b64decode(key_data)
+        except binascii.Error as e:
+            raise ValueError(f"Invalid base64 encoding in key data: {e}") from 
e
 
-        # Calculate SHA256 hash
-        digest = hashlib.sha256(public_bytes).digest()
-        fingerprint = base64.b64encode(digest).decode("utf-8").rstrip("=")
+        digest = hashlib.sha256(decoded_key_data).digest()
+        fingerprint_b64 = base64.b64encode(digest).decode("utf-8").rstrip("=")
 
-        # TODO: Do we really want to use a prefix?
-        return f"SHA256:{fingerprint}"
+        # Prefix follows the standard format
+        return f"SHA256:{fingerprint_b64}"
 
     raise ValueError("Invalid SSH key format")
 
diff --git a/playwright/test.py b/playwright/test.py
index 27d040d..fd6d865 100644
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -26,12 +26,15 @@ import re
 import socket
 import subprocess
 import urllib.parse
+from typing import Final
 
 import netifaces
 import rich.logging
 
 import playwright.sync_api as sync_api
 
+_SSH_KEY_PATH: Final[str] = "/root/.ssh/id_ed25519"
+
 
 @dataclasses.dataclass
 class Credentials:
@@ -141,7 +144,7 @@ def show_default_gateway_ip() -> None:
 
 
 def ssh_keys_generate() -> None:
-    ssh_key_path = "/root/.ssh/id_ed25519"
+    ssh_key_path = _SSH_KEY_PATH
     ssh_dir = os.path.dirname(ssh_key_path)
 
     try:
@@ -181,6 +184,7 @@ def test_all(page: sync_api.Page, credentials: Credentials) 
-> None:
     test_tidy_up(page)
     test_lifecycle(page)
     test_projects(page)
+    test_ssh(page)
 
 
 def test_lifecycle(page: sync_api.Page) -> None:
@@ -573,6 +577,72 @@ def test_tidy_up_release(page: sync_api.Page) -> None:
         logging.info("Could not find the tooling-0.1 release, no deletion 
needed")
 
 
+def test_ssh(page: sync_api.Page) -> None:
+    test_ssh_01_add_key(page)
+
+
+def test_ssh_01_add_key(page: sync_api.Page) -> None:
+    logging.info("Starting SSH key addition test")
+    go_to_path(page, "/")
+
+    logging.info("Navigating to Your Public Keys page")
+    page.locator('a[href="/keys"]:has-text("Your public keys")').click()
+    wait_for_path(page, "/keys")
+    logging.info("Navigated to Your Public Keys page")
+
+    logging.info("Clicking Add an SSH key button")
+    page.locator('a[href="/keys/ssh/add"]:has-text("Add an SSH key")').click()
+    wait_for_path(page, "/keys/ssh/add")
+    logging.info("Navigated to Add SSH Key page")
+
+    public_key_path = f"{_SSH_KEY_PATH}.pub"
+    try:
+        logging.info(f"Reading public key from {public_key_path}")
+        with open(public_key_path, encoding="utf-8") as f:
+            public_key_content = f.read().strip()
+        logging.info("Public key read successfully")
+    except OSError as e:
+        logging.error(f"Failed to read public key file {public_key_path}: {e}")
+        raise RuntimeError("Failed to read public key file") from e
+
+    logging.info("Pasting public key into textarea")
+    page.locator('textarea[name="key"]').fill(public_key_content)
+
+    logging.info("Submitting the Add SSH key form")
+    page.locator('input[type="submit"][value="Add SSH key"]').click()
+
+    logging.info("Waiting for navigation back to /keys page")
+    wait_for_path(page, "/keys")
+    logging.info("Navigated back to /keys page")
+
+    try:
+        logging.info("Calculating expected SSH key fingerprint using 
ssh-keygen -lf")
+        result = subprocess.run(
+            ["ssh-keygen", "-lf", public_key_path],
+            check=True,
+            capture_output=True,
+            text=True,
+        )
+        fingerprint_output = result.stdout.strip()
+        match = re.search(r"SHA256:([\w\+/=]+)", fingerprint_output)
+        if not match:
+            logging.error(f"Could not parse fingerprint from ssh-keygen 
output: {fingerprint_output}")
+            raise RuntimeError("Failed to parse SSH key fingerprint")
+        expected_fingerprint = f"SHA256:{match.group(1)}"
+        logging.info(f"Expected fingerprint: {expected_fingerprint}")
+
+    except (subprocess.CalledProcessError, FileNotFoundError, RuntimeError) as 
e:
+        logging.error(f"Failed to get SSH key fingerprint: {e}")
+        if isinstance(e, subprocess.CalledProcessError):
+            logging.error(f"ssh-keygen stderr: {e.stderr}")
+        raise RuntimeError("Failed to get SSH key fingerprint") from e
+
+    logging.info("Verifying that the added SSH key fingerprint is visible")
+    key_card_locator = 
page.locator(f'div.card:has(td:has-text("{expected_fingerprint}"))')
+    sync_api.expect(key_card_locator).to_be_visible()
+    logging.info("SSH key fingerprint verified successfully on /keys page")
+
+
 def wait_for_path(page: sync_api.Page, path: str) -> None:
     page.wait_for_load_state()
     parsed_url = urllib.parse.urlparse(page.url)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to