diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9744b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.coverage +htmlcov/ +.venv/ +venv/ +ENV/ +.env +.chelon/certs/ +*.tar.gz diff --git a/chelon.spec b/chelon.spec index beb73b3..3d5666c 100644 --- a/chelon.spec +++ b/chelon.spec @@ -1,6 +1,6 @@ Name: chelon Version: 1.0.0 -Release: 2%{?dist} +Release: 3%{?dist} Summary: Remote GPG package signing service License: GPL-2.0-or-later @@ -82,8 +82,7 @@ install -m 644 server/audit.py %{buildroot}%{_datadir}/%{name}/server/ install -m 755 tools/chelon-admin %{buildroot}%{_bindir}/ # Install client tools -install -m 755 tools/chelon-sign-rpm %{buildroot}%{_bindir}/ -install -m 755 tools/chelon-sign-repomd %{buildroot}%{_bindir}/ +install -m 755 tools/chelon-sign %{buildroot}%{_bindir}/ install -m 644 tools/chelon_client.py %{buildroot}%{_datadir}/%{name}/client/ # Install systemd unit @@ -133,11 +132,17 @@ fi %files client %doc README.md -%{_bindir}/chelon-sign-rpm -%{_bindir}/chelon-sign-repomd +%{_bindir}/chelon-sign %{_datadir}/%{name}/client/ %changelog +* Wed Jan 07 2026 Atomicorp - 1.0.0-3 +- Consolidate chelon-sign-rpm and chelon-sign-repomd into chelon-sign +- Security: Sanitize script paths in RPM macros +- Security: Optimize DoS protection with chunked reading +- Fix: Add error handling for malformed base64 signatures +- Fix: Improve client certificate fallback logic + * Wed Jan 07 2026 Atomicorp - 1.0.0-2 - Split into server and client subpackages - Add client signing tools (chelon-sign-rpm, chelon-sign-repomd) diff --git a/docs/USAGE.md b/docs/USAGE.md index ccc85e9..3f20383 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -38,15 +38,19 @@ scp /etc/chelon/certs/chelon_ca.crt ~/.chelon/certs/ ### Sign an RPM ```bash -# Sign a single RPM -chelon-sign-rpm package.rpm +# Sign a single RPM (detached signature) +chelon-sign package.rpm + +# Embed signature into RPM header (Integrated Signing) +# This allows 'rpm -K' to work natively +chelon-sign --resign package.rpm # Specify key type -chelon-sign-rpm --key-type legacy package.rpm +chelon-sign --key-type legacy package.rpm # Sign multiple RPMs for rpm in *.rpm; do - chelon-sign-rpm "$rpm" + chelon-sign "$rpm" done ``` @@ -71,11 +75,14 @@ Signature saved to: /tmp/tmp.xyz123 ### Sign Repository Metadata ```bash -# Sign repomd.xml -chelon-sign-repomd repodata/repomd.xml +# Sign repomd.xml (auto-detects type) +chelon-sign repodata/repomd.xml + +# Explicitly specify type +chelon-sign --type repodata repodata/repomd.xml # Specify key type -chelon-sign-repomd --key-type modern repodata/repomd.xml +chelon-sign --key-type modern repodata/repomd.xml ``` **Output:** @@ -222,11 +229,11 @@ sign_packages: # Sign all RPMs - for rpm in dist/*.rpm; do - chelon-sign-rpm "$rpm" + chelon-sign "$rpm" done # Sign repository metadata - - chelon-sign-repomd dist/repodata/repomd.xml + - chelon-sign dist/repodata/repomd.xml ``` ### Makefile Example @@ -237,10 +244,10 @@ RPMS := $(wildcard dist/*.rpm) sign: $(RPMS) @for rpm in $(RPMS); do \ echo "Signing $$rpm..."; \ - chelon-sign-rpm $$rpm || exit 1; \ + chelon-sign $$rpm || exit 1; \ done @echo "Signing repository metadata..." - @chelon-sign-repomd dist/repodata/repomd.xml + @chelon-sign dist/repodata/repomd.xml .PHONY: sign ``` @@ -257,13 +264,13 @@ CHELON_TOKEN="${CHELON_TOKEN:?CHELON_TOKEN not set}" # Sign all RPMs in directory for rpm in "$1"/*.rpm; do echo "Signing: $rpm" - chelon-sign-rpm "$rpm" + chelon-sign "$rpm" done # Sign repository metadata if [ -f "$1/repodata/repomd.xml" ]; then echo "Signing repository metadata" - chelon-sign-repomd "$1/repodata/repomd.xml" + chelon-sign "$1/repodata/repomd.xml" fi echo "All packages signed successfully" @@ -308,10 +315,10 @@ curl -k https://gamera:5050/api/v1/keys ```bash # Modern key (default) -chelon-sign-rpm package.rpm +chelon-sign package.rpm # Legacy key (explicit) -chelon-sign-rpm --key-type legacy package.rpm +chelon-sign --key-type legacy package.rpm ``` --- @@ -418,7 +425,7 @@ sudo firewall-cmd --list-all | grep 5050 # Sign all RPMs in parallel (careful with rate limits) find dist/ -name "*.rpm" | \ - xargs -P 4 -I {} chelon-sign-rpm {} + xargs -P 4 -I {} chelon-sign {} ``` ### Conditional Signing @@ -426,7 +433,7 @@ find dist/ -name "*.rpm" | \ ```bash # Only sign if not already signed if ! rpm -K package.rpm | grep -q "pgp"; then - chelon-sign-rpm package.rpm + chelon-sign package.rpm fi ``` diff --git a/server/__pycache__/audit.cpython-314.pyc b/server/__pycache__/audit.cpython-314.pyc deleted file mode 100644 index 3224717..0000000 Binary files a/server/__pycache__/audit.cpython-314.pyc and /dev/null differ diff --git a/server/__pycache__/auth.cpython-314.pyc b/server/__pycache__/auth.cpython-314.pyc deleted file mode 100644 index 294b8bb..0000000 Binary files a/server/__pycache__/auth.cpython-314.pyc and /dev/null differ diff --git a/server/__pycache__/chelon-service.cpython-314.pyc b/server/__pycache__/chelon-service.cpython-314.pyc deleted file mode 100644 index 5ba1820..0000000 Binary files a/server/__pycache__/chelon-service.cpython-314.pyc and /dev/null differ diff --git a/server/__pycache__/signing_engine.cpython-314.pyc b/server/__pycache__/signing_engine.cpython-314.pyc deleted file mode 100644 index 2a6bd42..0000000 Binary files a/server/__pycache__/signing_engine.cpython-314.pyc and /dev/null differ diff --git a/server/auth.py b/server/auth.py index 420189a..c3cda92 100644 --- a/server/auth.py +++ b/server/auth.py @@ -151,9 +151,16 @@ def validate_token(self, token: str) -> Dict: token_id, secret = token.split(':', 1) - # Check if token exists + # Check if token exists, reload if not found (to handle new tokens without restart) if token_id not in self.tokens: - raise ValueError(f"Unknown token: {token_id}") + with self._lock: + # Double-check inside lock + if token_id not in self.tokens: + logger.info(f"Token {token_id} not found in memory, reloading from {self.tokens_file}") + self.tokens = self._load_tokens() + # After reloading, immediately check if the token now exists + if token_id not in self.tokens: + raise ValueError(f"Unknown token after reload: {token_id}") token_info = self.tokens[token_id] diff --git a/server/chelon-service.py b/server/chelon-service.py index ab4a785..9087859 100644 --- a/server/chelon-service.py +++ b/server/chelon-service.py @@ -357,16 +357,23 @@ def sign_repodata(): if __name__ == '__main__': # Run the Flask app - host = os.environ.get('CHELON_HOST', '127.0.0.1') - port = int(os.environ.get('CHELON_PORT', 5050)) + # Prioritize config file over environment variables + host = config.get('CHELON_HOST') or os.environ.get('CHELON_HOST', '127.0.0.1') + port = int(config.get('CHELON_PORT') or os.environ.get('CHELON_PORT') or 5050) logger.info(f"Starting Chelon service on {host}:{port}") - # SSL/TLS Configuration - ssl_cert = os.environ.get('CHELON_SSL_CERT') - ssl_key = os.environ.get('CHELON_SSL_KEY') - ssl_ca = os.environ.get('CHELON_SSL_CA') - verify_client = os.environ.get('CHELON_VERIFY_CLIENT', 'false').lower() == 'true' + # SSL/TLS Configuration - Prefer config file, fall back to environment + ssl_cert = config.get('CHELON_SSL_CERT') or os.environ.get('CHELON_SSL_CERT') + ssl_key = config.get('CHELON_SSL_KEY') or os.environ.get('CHELON_SSL_KEY') + ssl_ca = config.get('CHELON_SSL_CA') or os.environ.get('CHELON_SSL_CA') + + # Support both names for backward compatibility/consistency + # Precedence: config['CHELON_SSL_VERIFY_CLIENT'] > config['CHELON_VERIFY_CLIENT'] > env['CHELON_VERIFY_CLIENT'] + verify_client_val = (config.get('CHELON_SSL_VERIFY_CLIENT') or + config.get('CHELON_VERIFY_CLIENT') or + os.environ.get('CHELON_VERIFY_CLIENT', 'false')) + verify_client = str(verify_client_val).lower() == 'true' ssl_context = None if ssl_cert and ssl_key: diff --git a/tools/__pycache__/chelon_client.cpython-314.pyc b/tools/__pycache__/chelon_client.cpython-314.pyc deleted file mode 100644 index 0ea13ff..0000000 Binary files a/tools/__pycache__/chelon_client.cpython-314.pyc and /dev/null differ diff --git a/tools/chelon-sign b/tools/chelon-sign new file mode 100755 index 0000000..6e8fe00 --- /dev/null +++ b/tools/chelon-sign @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +""" +Chelon Sign (Unified Client) + +Signs files using the Chelon service. +Supports: +- RPM packages (detached signatures or embedded via rpmsign) +- Repository metadata (repomd.xml) +- GPG emulation for rpmsign integration +""" + +import os +import sys +import argparse +import subprocess +import base64 +import shlex +import binascii +from pathlib import Path +from typing import Optional, List + +# Add tools directory to path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +try: + from chelon_client import get_client, ChelonClientError +except ImportError: + # Handle direct execution if not in path + try: + from tools.chelon_client import get_client, ChelonClientError + except ImportError: + print("Error: Could not import chelon_client. Ensure tools/ is in PYTHONPATH.", file=sys.stderr) + sys.exit(1) + + +def get_gpg_version() -> str: + """ + Get the version of the system gpg command. + Returns a generic version if gpg is not found. + """ + try: + result = subprocess.run(['gpg', '--version'], capture_output=True, text=True, check=True) + first_line = result.stdout.splitlines()[0] + if 'gpg (GnuPG)' in first_line: + return first_line.split()[-1] + except (subprocess.CalledProcessError, FileNotFoundError, IndexError): + # If gpg is missing or its output is unexpected, fall back to a default version string. + pass + return "2.4.4" + + +def gpg_mode(args: List[str]): + """ + Emulate GPG behavior when called by rpmsign. + rpmsign calls gpg with flags like: + --no-verbose --no-armor --batch --no-tty -u -sbo -- - + """ + output_file = None + input_file = "-" + + # Simple argument parser for GPG flags + i = 0 + while i < len(args): + arg = args[i] + if arg in ('-o', '--output'): + if i + 1 < len(args): + output_file = args[i+1] + i += 2 + else: + print(f"Error: {arg} requires an argument", file=sys.stderr) + sys.exit(1) + elif arg == '-sbo': + if i + 1 < len(args): + output_file = args[i+1] + i += 2 + else: + print(f"Error: {arg} requires an argument", file=sys.stderr) + sys.exit(1) + elif arg == '-u': + if i + 1 < len(args): + # key_id is parsed but we currently rely on environment variable CHELON_KEY_TYPE + # as per the wrapper logic, but we should at least parse it correctly. + # key_id = args[i+1] + i += 2 + else: + print(f"Error: {arg} requires an argument", file=sys.stderr) + sys.exit(1) + elif arg == '--': + if i + 1 < len(args): + input_file = args[i+1] + break + elif arg == '--version': + version = get_gpg_version() + print(f"gpg (GnuPG) {version}") + sys.exit(0) + else: + i += 1 + + # Read input data with a 10MB limit (DoS protection) + MAX_INPUT_SIZE = 10 * 1024 * 1024 # 10MB + if input_file == "-": + # Read stdin in chunks to avoid buffering MAX_INPUT_SIZE+1 bytes unconditionally + MAX_CHUNK_SIZE = 64 * 1024 # 64KB + data_buf = bytearray() + stdin_buffer = sys.stdin.buffer + + while True: + # Limit each read to the remaining allowed bytes plus one extra for overflow detection + remaining = (MAX_INPUT_SIZE + 1) - len(data_buf) + if remaining <= 0: + break + + chunk = stdin_buffer.read(min(MAX_CHUNK_SIZE, remaining)) + if not chunk: + break + + data_buf.extend(chunk) + + if len(data_buf) > MAX_INPUT_SIZE: + print(f"Error: Input data from stdin exceeds limit of {MAX_INPUT_SIZE} bytes", file=sys.stderr) + sys.exit(1) + + data = bytes(data_buf) + else: + file_size = os.path.getsize(input_file) + if file_size > MAX_INPUT_SIZE: + print(f"Error: Input file too large ({file_size} bytes)", file=sys.stderr) + sys.exit(1) + with open(input_file, 'rb') as f: + data = f.read() + + # Determine key type from key_id (mapping logic) + # In this wrapper, we rely on env CHELON_KEY_TYPE or default to modern + key_type = os.environ.get('CHELON_KEY_TYPE', 'modern') + + # Initialize client + try: + client = get_client() + response = client.sign_data(data, key_type=key_type, operation='rpm') + signature = response['signature'] + + # rpmsign usually expects binary signature if --armor is not passed + # chelon-gpg-wrapper.sh handles this by checking for --armor + is_armored = '--armor' in args or '-a' in args + + if output_file and output_file != '-': + try: + if is_armored: + with open(output_file, 'w') as f: + f.write(signature) + else: + # Strip headers and decode if binary expected + if signature.startswith('-----BEGIN'): + # Need to dearmor + try: + result = subprocess.run( + ['gpg', '--dearmor'], + input=signature.encode('utf-8'), + capture_output=True, + check=True + ) + with open(output_file, 'wb') as f: + f.write(result.stdout) + except subprocess.CalledProcessError as e: + print(f"Error: Failed to dearmor signature: {e.stderr.decode()}", file=sys.stderr) + sys.exit(1) + except FileNotFoundError: + print("Error: 'gpg' command not found. Please install GnuPG to handle armored signatures in binary mode.", file=sys.stderr) + sys.exit(1) + else: + with open(output_file, 'wb') as f: + try: + f.write(base64.b64decode(signature)) + except binascii.Error as e: + print(f"Error: Malformed base64 signature: {e}", file=sys.stderr) + sys.exit(1) + except IOError as e: + print(f"Error writing to {output_file}: {e}", file=sys.stderr) + sys.exit(1) + else: + if is_armored: + sys.stdout.write(signature) + sys.stdout.flush() + else: + if signature.startswith('-----BEGIN'): + try: + result = subprocess.run( + ['gpg', '--dearmor'], + input=signature.encode('utf-8'), + capture_output=True, + check=True + ) + sys.stdout.buffer.write(result.stdout) + except subprocess.CalledProcessError as e: + print(f"Error: Failed to dearmor signature: {e.stderr.decode()}", file=sys.stderr) + sys.exit(1) + except FileNotFoundError: + print("Error: 'gpg' command not found.", file=sys.stderr) + sys.exit(1) + else: + try: + sys.stdout.buffer.write(base64.b64decode(signature)) + except binascii.Error as e: + print(f"Error: Malformed base64 signature: {e}", file=sys.stderr) + sys.exit(1) + sys.stdout.buffer.flush() + + except ChelonClientError as e: + print(f"GPG Emulation Error (signing service): {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"GPG Emulation Error: {e}", file=sys.stderr) + sys.exit(1) + + sys.exit(0) + + +def sign_rpm_integrated(rpm_path: str, key_type: str = 'modern', verbose: bool = False): + """ + Embed signature into RPM using rpmsign and self as wrapper. + """ + rpm_file = Path(rpm_path).absolute() + script_path = Path(__file__).absolute() + + # Map key_type to standard Atomicorp Key IDs if not provided + key_ids = { + 'legacy': '4520AFA9', + 'modern': 'CB2C73F04F3BE076' + } + key_id = key_ids.get(key_type, key_type) + + if verbose: + print(f"Integrated Signing: {rpm_file}") + print(f"Using Key: {key_type} ({key_id})") + + # Prepare environment for the wrapper call + new_env = os.environ.copy() + new_env['CHELON_KEY_TYPE'] = key_type + + cmd = [ + 'rpmsign', + '--define', f'__gpg {shlex.quote(str(script_path))}', + '--define', f'_gpg_name {key_id}', + '--define', '_gpg_sign_cmd_extra_args --batch --no-tty', + '--resign', str(rpm_file) + ] + + try: + subprocess.run(cmd, check=True, capture_output=not verbose, text=True, env=new_env) + if verbose: + print("✓ Signature embedded successfully") + return True + except subprocess.CalledProcessError as e: + error_msg = e.stderr.strip() if e.stderr else str(e) + if not error_msg and not verbose: + error_msg = "rpmsign failed (check system logs or run with --verbose)" + print(f"Error during integrated signing: {error_msg}", file=sys.stderr) + return False + except Exception as e: + print(f"Unexpected error during integrated signing: {e}", file=sys.stderr) + return False + + +def sign_file_detached(file_path: str, output_path: Optional[str] = None, + key_type: str = 'modern', operation: str = 'rpm', verbose: bool = False) -> str: + """ + Sign a file (creates detached signature) + Supports both rpm and repodata operations + """ + target_file = Path(file_path) + if not target_file.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + if output_path is None: + output_path = str(target_file) + '.asc' + + if verbose: + print(f"Signing: {file_path}") + print(f"Output: {output_path}") + print(f"Key type: {key_type}") + print(f"Operation: sign_{operation}") + + try: + client = get_client() + response = client.sign_file(str(target_file), key_type=key_type, operation=operation) + + try: + with open(output_path, 'w') as f: + f.write(response['signature']) + except OSError as e: + print(f"Error writing signature to '{output_path}': {e}", file=sys.stderr) + sys.exit(1) + + if verbose: + print(f"✓ Signed successfully") + print(f" Request ID: {response.get('request_id')}") + print(f" Key ID: {response.get('key_id')}") + print(f" Signature written to: {output_path}") + + return output_path + + except ChelonClientError as e: + print(f"Error signing file: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + # Detect if we are being called as a GPG wrapper + if len(sys.argv) > 1: + # Check if it looks specifically like a GPG call from rpmsign + gpg_flags = {'-sbo', '--detach-sign', '--armor', '--no-secmem-warning'} + if any(arg in gpg_flags for arg in sys.argv): + gpg_mode(sys.argv[1:]) + elif sys.argv[1] == '--version' and len(sys.argv) == 2: + # Handle standalone --version for discovery tools + gpg_mode(sys.argv[1:]) + + parser = argparse.ArgumentParser( + description='Chelon Sign - Unified Signing Tool', + epilog='Environment variables: CHELON_URL, CHELON_TOKEN, CHELON_CERT_DIR' + ) + + parser.add_argument('file', help='Path to file (RPM or repomd.xml)') + parser.add_argument('-t', '--type', choices=['rpm', 'repodata'], + help='Signing type (default: guess from extension or "rpm")') + parser.add_argument('--resign', action='store_true', help='Embed signature into RPM header (requires rpmsign, implies --type rpm)') + parser.add_argument('-o', '--output', help='Output signature file (default: .asc, only for detached)') + parser.add_argument('-k', '--key-type', choices=['legacy', 'modern'], default='modern', + help='GPG key type to use (default: modern)') + parser.add_argument('--insecure', action='store_true', help='Disable SSL certificate verification') + parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') + + args = parser.parse_args() + + if args.insecure: + os.environ['CHELON_VERIFY_SSL'] = 'false' + + # Determine operation type + op_type = args.type + if not op_type: + if args.resign: + op_type = 'rpm' + elif args.file.endswith('.xml'): + op_type = 'repodata' + else: + op_type = 'rpm' + + try: + if args.resign: + if op_type != 'rpm': + print("Error: --resign is only supported for RPM files", file=sys.stderr) + return 1 + success = sign_rpm_integrated(args.file, key_type=args.key_type, verbose=args.verbose) + return 0 if success else 1 + else: + output_file = sign_file_detached( + args.file, + output_path=args.output, + key_type=args.key_type, + operation=op_type, + verbose=args.verbose + ) + if not args.verbose: + print(output_file) + return 0 + + except KeyboardInterrupt: + print("\nInterrupted by user", file=sys.stderr) + return 130 + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/chelon-sign-repomd b/tools/chelon-sign-repomd deleted file mode 100755 index 7464f08..0000000 --- a/tools/chelon-sign-repomd +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -""" -Chelon Sign Repomd - -Sign RPM repository metadata files (repomd.xml) using Chelon service. -Creates detached GPG signatures (.asc files). -""" - -import os -import sys -import argparse -from pathlib import Path -from typing import Optional - -# Add tools directory to path for chelon_client import -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from chelon_client import get_client, ChelonClientError - - -def sign_repomd(repomd_path: str, output_path: Optional[str] = None, - key_type: str = 'modern', verbose: bool = False) -> str: - """ - Sign a repomd.xml file - - Args: - repomd_path: Path to repomd.xml file - output_path: Path for signature file (default: repomd_path + '.asc') - key_type: GPG key type to use - verbose: Print verbose output - - Returns: - Path to signature file - """ - repomd_file = Path(repomd_path) - - if not repomd_file.exists(): - raise FileNotFoundError(f"Repomd file not found: {repomd_path}") - - if output_path is None: - output_path = str(repomd_file) + '.asc' - - if verbose: - print(f"Signing: {repomd_path}") - print(f"Output: {output_path}") - print(f"Key type: {key_type}") - - # Get client - try: - client = get_client() - except ChelonClientError as e: - print(f"Error initializing client: {e}", file=sys.stderr) - sys.exit(1) - - # Sign the file - try: - if verbose: - print("Sending signing request...") - - response = client.sign_file(str(repomd_file), key_type=key_type, operation='repodata') - - if verbose: - print(f"✓ Signed successfully") - print(f" Request ID: {response.get('request_id')}") - print(f" Key ID: {response.get('key_id')}") - print(f" Key Fingerprint: {response.get('key_fingerprint')}") - - # Write signature to file - signature = response['signature'] - with open(output_path, 'w') as f: - f.write(signature) - - if verbose: - print(f"✓ Signature written to: {output_path}") - - return output_path - - except ChelonClientError as e: - print(f"Error signing file: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: - print(f"Unexpected error: {e}", file=sys.stderr) - sys.exit(1) - - -def main(): - parser = argparse.ArgumentParser( - description='Sign RPM repository metadata using Chelon service', - epilog='Environment variables: CHELON_URL, CHELON_TOKEN, CHELON_CERT_DIR' - ) - - parser.add_argument('repomd_file', help='Path to repomd.xml file') - parser.add_argument('-o', '--output', help='Output signature file (default: .asc)') - parser.add_argument('-k', '--key-type', choices=['legacy', 'modern'], default='modern', - help='GPG key type to use (default: modern)') - parser.add_argument('--insecure', action='store_true', help='Disable SSL certificate verification') - parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') - - args = parser.parse_args() - - # Set verify_ssl based on --insecure flag - if args.insecure: - os.environ['CHELON_VERIFY_SSL'] = 'false' - - try: - output_file = sign_repomd( - args.repomd_file, - output_path=args.output, - key_type=args.key_type, - verbose=args.verbose - ) - - if not args.verbose: - print(output_file) - - return 0 - - except KeyboardInterrupt: - print("\nInterrupted", file=sys.stderr) - return 130 - except Exception as e: - print(f"Error: {e}", file=sys.stderr) - return 1 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/tools/chelon-sign-rpm b/tools/chelon-sign-rpm deleted file mode 100755 index 3f788c3..0000000 --- a/tools/chelon-sign-rpm +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -""" -Chelon Sign RPM - -Sign RPM packages using Chelon service. -Creates detached GPG signatures (.asc files). - -Note: This creates detached signatures. For embedded RPM signatures, -you'll need to use rpm-sign library or rpmsign command with proper integration. -""" - -import os -import sys -import argparse -from pathlib import Path -from typing import Optional - -# Add tools directory to path -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from chelon_client import get_client, ChelonClientError - - -def sign_rpm(rpm_path: str, output_path: Optional[str] = None, - key_type: str = 'modern', verbose: bool = False) -> str: - """ - Sign an RPM file (creates detached signature) - - Args: - rpm_path: Path to RPM file - output_path: Path for signature file (default: rpm_path + '.asc') - key_type: GPG key type to use - verbose: Print verbose output - - Returns: - Path to signature file - """ - rpm_file = Path(rpm_path) - - if not rpm_file.exists(): - raise FileNotFoundError(f"RPM file not found: {rpm_path}") - - if output_path is None: - output_path = str(rpm_file) + '.asc' - - if verbose: - print(f"Signing: {rpm_path}") - print(f"Output: {output_path}") - print(f"Key type: {key_type}") - print(f"Note: Creating detached signature (not embedding in RPM)") - - # Get client - try: - client = get_client() - except ChelonClientError as e: - print(f"Error initializing client: {e}", file=sys.stderr) - sys.exit(1) - - # Sign the file - try: - if verbose: - file_size = rpm_file.stat().st_size - print(f"RPM size: {file_size:,} bytes") - print("Sending signing request...") - - response = client.sign_file(str(rpm_file), key_type=key_type, operation='rpm') - - if verbose: - print(f"✓ Signed successfully") - print(f" Request ID: {response.get('request_id')}") - print(f" Key ID: {response.get('key_id')}") - print(f" Key Fingerprint: {response.get('key_fingerprint')}") - - # Write signature to file - signature = response['signature'] - with open(output_path, 'w') as f: - f.write(signature) - - if verbose: - print(f"✓ Signature written to: {output_path}") - print() - print("To verify: gpg --verify", output_path, rpm_path) - - return output_path - - except ChelonClientError as e: - print(f"Error signing file: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: - print(f"Unexpected error: {e}", file=sys.stderr) - sys.exit(1) - - -def main(): - parser = argparse.ArgumentParser( - description='Sign RPM packages using Chelon service (creates detached signatures)', - epilog='Environment variables: CHELON_URL, CHELON_TOKEN, CHELON_CERT_DIR' - ) - - parser.add_argument('rpm_file', help='Path to RPM file') - parser.add_argument('-o', '--output', help='Output signature file (default: .asc)') - parser.add_argument('-k', '--key-type', choices=['legacy', 'modern'], default='modern', - help='GPG key type to use (default: modern)') - parser.add_argument('--insecure', action='store_true', help='Disable SSL certificate verification') - parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') - - args = parser.parse_args() - - # Set verify_ssl based on --insecure flag - if args.insecure: - os.environ['CHELON_VERIFY_SSL'] = 'false' - - try: - output_file = sign_rpm( - args.rpm_file, - output_path=args.output, - key_type=args.key_type, - verbose=args.verbose - ) - - if not args.verbose: - print(output_file) - - return 0 - - except KeyboardInterrupt: - print("\nInterrupted", file=sys.stderr) - return 130 - except Exception as e: - print(f"Error: {e}", file=sys.stderr) - if args.verbose: - import traceback - traceback.print_exc() - return 1 - - -if __name__ == '__main__': - sys.exit(main()) - diff --git a/tools/chelon_client.py b/tools/chelon_client.py index eab43ea..6d0c993 100644 --- a/tools/chelon_client.py +++ b/tools/chelon_client.py @@ -53,12 +53,24 @@ def __init__(self, raise ChelonClientError("No token provided. Set CHELON_TOKEN environment variable or pass token parameter.") # Validate certificate files exist - self.client_cert = self.cert_dir / 'client.crt' - self.client_key = self.cert_dir / 'client.key' - self.ca_cert = self.cert_dir / 'ca.crt' + self.client_cert = self.cert_dir / 'chelon_client.crt' + self.client_key = self.cert_dir / 'chelon_client.key' + self.ca_cert = self.cert_dir / 'chelon_ca.crt' if not self.client_cert.exists(): - raise ChelonClientError(f"Client certificate not found: {self.client_cert}") + # Fallback to older names for backward compatibility if new names don't exist + alt_cert = self.cert_dir / 'client.crt' + alt_key = self.cert_dir / 'client.key' + alt_ca = self.cert_dir / 'ca.crt' + + if alt_cert.exists() and alt_key.exists(): + self.client_cert = alt_cert + self.client_key = alt_key + if alt_ca.exists(): + self.ca_cert = alt_ca + else: + raise ChelonClientError(f"Client certificate not found: {self.client_cert}") + if not self.client_key.exists(): raise ChelonClientError(f"Client key not found: {self.client_key}") if self.verify_ssl and not self.ca_cert.exists():