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
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()
def extract_commit_author(message):
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_TARGET = b"author "
for line in message_lines:
AUTHOR_TARGET = b"author "
if not line.strip(b""): break
elif line.startswith(AUTHOR_TARGET):
try:
@@ -235,10 +222,14 @@ def verify(args):
author = line[spos+1:epos].decode("utf-8")
break
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_TARGET = b"committer "
for line in message_lines:
COMMITTER_TARGET = b"committer "
if not line.strip(b""): break
elif line.startswith(COMMITTER_TARGET):
try:
@@ -247,7 +238,11 @@ def verify(args):
committer = line[spos+1:epos].decode("utf-8")
break
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 = ""
is_tag = False
for line in message_lines:
@@ -263,6 +258,26 @@ def verify(args):
break
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
rsg = ssh_sig["signature_data"]
+99 -6
View File
@@ -34,6 +34,8 @@ import threading
import subprocess
import urllib.parse
import RNS
import struct
import base64
from collections import deque
from datetime import datetime
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 import umsgpack as mp
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():
APP_NAME = "nomadnetwork"
@@ -958,6 +962,15 @@ class NomadNetworkNode():
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")
# 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
if commit_info.get("parents"):
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_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"Date: {commit_info['author_date']}\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"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"):
content_parts.append(f"Commit: {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"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("\n")
@@ -2197,7 +2211,8 @@ class NomadNetworkNode():
"committer_date": lines[6],
"message": "\n".join(lines[7:]).strip(),
"files": [],
"diff": None }
"diff": None,
"signature_status": None }
# Get file change statistics
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)
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):
readme_names = [ ("README.mu", False), ("Readme.mu", False), ("readme.mu", False), ("README", False),
("readme", False), ("README.md", True), ("readme.md", True), ("README.rst", False),