Show signature status on commit page

This commit is contained in:
Mark Qvist
2026-05-28 02:09:47 +02:00
parent 237eada209
commit bcf35030bc
2 changed files with 132 additions and 24 deletions
+33 -18
View File
@@ -209,24 +209,11 @@ def check_novalidate(args):
except Exception: return 1 except Exception: return 1
def verify(args): def extract_commit_author(message):
sigfile = args.sigfile
principal = args.principal
if not sigfile or not os.path.isfile(sigfile): print("Error: Signature file not found", file=sys.stderr); return 1
message = sys.stdin.buffer.read()
message_lines = message.splitlines() message_lines = message.splitlines()
try:
with open(sigfile, 'r') as f: armored_sig = f.read()
raw_sig = unarmor_ssh_signature(armored_sig)
ssh_sig = parse_ssh_signature(raw_sig)
except Exception as e: print(f"Error parsing signature: {e}", file=sys.stderr); return 1
author = "" author = ""
AUTHOR_TARGET = b"author "
for line in message_lines: for line in message_lines:
AUTHOR_TARGET = b"author "
if not line.strip(b""): break if not line.strip(b""): break
elif line.startswith(AUTHOR_TARGET): elif line.startswith(AUTHOR_TARGET):
try: try:
@@ -235,10 +222,14 @@ def verify(args):
author = line[spos+1:epos].decode("utf-8") author = line[spos+1:epos].decode("utf-8")
break break
except Exception as e: print(f"Error while determining author from signed commit"); return 1 except Exception as e: print(f"Error while determining author from signed commit"); return 1
return author
def extract_commit_committer(message):
message_lines = message.splitlines()
committer = "" committer = ""
COMMITTER_TARGET = b"committer "
for line in message_lines: for line in message_lines:
COMMITTER_TARGET = b"committer "
if not line.strip(b""): break if not line.strip(b""): break
elif line.startswith(COMMITTER_TARGET): elif line.startswith(COMMITTER_TARGET):
try: try:
@@ -247,7 +238,11 @@ def verify(args):
committer = line[spos+1:epos].decode("utf-8") committer = line[spos+1:epos].decode("utf-8")
break break
except Exception as e: print(f"Error while determining committer from signed commit"); return 1 except Exception as e: print(f"Error while determining committer from signed commit"); return 1
return committer
def extract_commit_tagger(message):
message_lines = message.splitlines()
tagger = "" tagger = ""
is_tag = False is_tag = False
for line in message_lines: for line in message_lines:
@@ -263,6 +258,26 @@ def verify(args):
break break
except Exception as e: print(f"Error while determining tagger from signed commit"); return 1 except Exception as e: print(f"Error while determining tagger from signed commit"); return 1
return tagger, is_tag
def verify(args):
sigfile = args.sigfile
principal = args.principal
if not sigfile or not os.path.isfile(sigfile): print("Error: Signature file not found", file=sys.stderr); return 1
message = sys.stdin.buffer.read()
try:
with open(sigfile, 'r') as f: armored_sig = f.read()
raw_sig = unarmor_ssh_signature(armored_sig)
ssh_sig = parse_ssh_signature(raw_sig)
except Exception as e: print(f"Error parsing signature: {e}", file=sys.stderr); return 1
author = extract_commit_author(message)
committer = extract_commit_committer(message)
tagger, is_tag = extract_commit_tagger(message)
if ssh_sig["namespace"] != NAMESPACE_GIT: print(f"Invalid commit signature namespace", file=sys.stderr); return 1 if ssh_sig["namespace"] != NAMESPACE_GIT: print(f"Invalid commit signature namespace", file=sys.stderr); return 1
rsg = ssh_sig["signature_data"] rsg = ssh_sig["signature_data"]
+99 -6
View File
@@ -34,6 +34,8 @@ import threading
import subprocess import subprocess
import urllib.parse import urllib.parse
import RNS import RNS
import struct
import base64
from collections import deque from collections import deque
from datetime import datetime from datetime import datetime
from RNS.Utilities.rngit import APP_NAME from RNS.Utilities.rngit import APP_NAME
@@ -42,6 +44,8 @@ from RNS.Utilities.rngit.highlight import SyntaxHighlighter
from RNS.vendor.configobj import ConfigObj from RNS.vendor.configobj import ConfigObj
from RNS.vendor import umsgpack as mp from RNS.vendor import umsgpack as mp
from RNS._version import __version__ from RNS._version import __version__
from RNS.Utilities.rnid import validate_rsg, extract_signed_rsg_data
from RNS.Utilities.rngit.commitsigs import unarmor_ssh_signature, parse_ssh_signature, extract_commit_author
class NomadNetworkNode(): class NomadNetworkNode():
APP_NAME = "nomadnetwork" APP_NAME = "nomadnetwork"
@@ -958,6 +962,15 @@ class NomadNetworkNode():
i_folder = self.icon("folder") i_folder = self.icon("folder")
content_parts.append(f"{self.m_link(f'{i_folder} Browse tree at this commit', self.PATH_TREE, g=group_name, r=repo_name, ref=resolved_hash)}\n\n") content_parts.append(f"{self.m_link(f'{i_folder} Browse tree at this commit', self.PATH_TREE, g=group_name, r=repo_name, ref=resolved_hash)}\n\n")
# Validate and display commit signature status
show_sig = False
sig_status = self.get_commit_signature(repo_path, resolved_hash)
if sig_status["signed"]:
if sig_status["valid"] and sig_status["author_match"]: sig_text = f"`FT66BB85Valid, signed by author`f"; show_sig = True
elif sig_status["valid"]: sig_text = f"`Faa0{self.m_escape(sig_status['message'])}`f"; show_sig = True
else: sig_text = f"`F900{self.m_escape(sig_status['message'])}`f"; show_sig = True
else: sig_text = "Not signed"
# Commit metadata # Commit metadata
if commit_info.get("parents"): if commit_info.get("parents"):
parent_links = [] parent_links = []
@@ -965,14 +978,15 @@ class NomadNetworkNode():
parent_link = self.m_link(parent_hash[:7], self.PATH_COMMIT, g=group_name, r=repo_name, ref=ref, h=parent_hash) parent_link = self.m_link(parent_hash[:7], self.PATH_COMMIT, g=group_name, r=repo_name, ref=ref, h=parent_hash)
parent_links.append(parent_link) parent_links.append(parent_link)
content_parts.append(f"Parents: {' '.join(parent_links)}\n") content_parts.append(f"Parents : {' '.join(parent_links)}\n")
content_parts.append(f"Author: {self.m_escape(commit_info['author_name'])} <{self.m_escape(commit_info['author_email'])}>\n") content_parts.append(f"Author : {self.m_escape(commit_info['author_name'])} <{self.m_escape(commit_info['author_email'])}>\n")
content_parts.append(f"Date: {commit_info['author_date']}\n") content_parts.append(f"Signature : {sig_text}\n") if show_sig else None
content_parts.append(f"Date : {commit_info['author_date']}\n")
if commit_info.get("committer_name") != commit_info.get("author_name"): if commit_info.get("committer_name") != commit_info.get("author_name"):
content_parts.append(f"Commit: {self.m_escape(commit_info['committer_name'])} <{self.m_escape(commit_info['committer_email'])}>\n") content_parts.append(f"Committer : {self.m_escape(commit_info['committer_name'])} <{self.m_escape(commit_info['committer_email'])}>\n")
content_parts.append(f"Date: {commit_info['committer_date']}\n") content_parts.append(f"Date : {commit_info['committer_date']}\n")
content_parts.append("\n") content_parts.append("\n")
@@ -2197,7 +2211,8 @@ class NomadNetworkNode():
"committer_date": lines[6], "committer_date": lines[6],
"message": "\n".join(lines[7:]).strip(), "message": "\n".join(lines[7:]).strip(),
"files": [], "files": [],
"diff": None } "diff": None,
"signature_status": None }
# Get file change statistics # Get file change statistics
stats_result = subprocess.run(["git", "diff-tree", "--numstat", "-r", commit_hash], stats_result = subprocess.run(["git", "diff-tree", "--numstat", "-r", commit_hash],
@@ -2267,6 +2282,84 @@ class NomadNetworkNode():
RNS.log(f"Error getting commit info: {e}", RNS.LOG_WARNING) RNS.log(f"Error getting commit info: {e}", RNS.LOG_WARNING)
return None return None
def get_commit_signature(self, repo_path, commit_hash):
try:
result = subprocess.run(["git", "cat-file", "-p", commit_hash],
cwd=repo_path, capture_output=True, text=True,
timeout=self.GIT_COMMAND_TIMEOUT, check=False)
if result.returncode != 0:
return {"signed": False, "valid": False, "signer_hash": None,
"author_match": False, "message": "Could not read commit object"}
commit_content = result.stdout
lines = commit_content.split("\n")
sig_lines = []
in_signature = False
signed_content_lines = []
for line in lines:
if line.startswith("gpgsig ") or line.startswith("gpgsig-sha256 "):
in_signature = True
sig_start = line.find(" ") + 1
sig_lines.append(line[sig_start:])
elif in_signature:
if line.startswith(" "): sig_lines.append(line[1:])
else:
in_signature = False
signed_content_lines.append(line)
else: signed_content_lines.append(line)
if not sig_lines:
return {"signed": False, "valid": False, "signer_hash": None,
"author_match": False, "message": "Not signed"}
armored_sig = "\n".join(sig_lines)
signed_content = "\n".join(signed_content_lines).encode("utf-8")
try:
sig_data = unarmor_ssh_signature(armored_sig)
try: rsg = parse_ssh_signature(sig_data)["signature_data"]
except ValueError as e:
return {"signed": True, "valid": False, "signer_hash": None,
"author_match": False, "message": "Malformed SSH wrapping for RSG data"}
valid, signed_rsg_data, signing_identity = validate_rsg(rsg, signed_content)
if not valid:
return {"signed": True, "valid": False, "signer_hash": None,
"author_match": False, "message": "Invalid signature"}
signer_hash = RNS.hexrep(signing_identity.hash, delimit=False)
author = extract_commit_author(signed_content)
if not author:
return {"signed": True, "valid": True, "signer_hash": signer_hash,
"author_match": False, "message": "Could not verify author"}
if author == signer_hash:
return {"signed": True, "valid": True, "signer_hash": signer_hash,
"author_match": True, "message": f"Valid, signed by <{signer_hash}>"}
else:
return {"signed": True, "valid": True, "signer_hash": signer_hash,
"author_match": False, "message": f"Invalid signer <{signer_hash}>, author is <{author}>"}
except Exception as e:
RNS.log(f"Error validating commit signature: {e}", RNS.LOG_DEBUG)
return {"signed": True, "valid": False, "signer_hash": None,
"author_match": False, "message": "Signature validation error"}
except subprocess.TimeoutExpired:
RNS.log(f"Timeout checking commit signature", RNS.LOG_WARNING)
return {"signed": False, "valid": False, "signer_hash": None,
"author_match": False, "message": "Timeout"}
except Exception as e:
RNS.log(f"Error checking commit signature: {e}", RNS.LOG_DEBUG)
return {"signed": False, "valid": False, "signer_hash": None,
"author_match": False, "message": "Error"}
def get_readme_content(self, repo_path): def get_readme_content(self, repo_path):
readme_names = [ ("README.mu", False), ("Readme.mu", False), ("readme.mu", False), ("README", False), readme_names = [ ("README.mu", False), ("Readme.mu", False), ("readme.mu", False), ("README", False),
("readme", False), ("README.md", True), ("readme.md", True), ("README.rst", False), ("readme", False), ("README.md", True), ("readme.md", True), ("README.rst", False),