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]