mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-07-18 06:18:11 -07:00
Show signature status on commit page
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user