From e4d617995d96dfbecaf38514c79148cf56ad4c2c Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Wed, 7 Jan 2026 17:09:26 -0500 Subject: [PATCH 01/13] Fixes for SSL support and signing workflow Signed-off-by: Scott R. Shinn --- docs/USAGE.md | 6 +- server/auth.py | 6 +- server/chelon-service.py | 20 +- .../__pycache__/chelon_client.cpython-314.pyc | Bin 10238 -> 10683 bytes tools/chelon-sign-rpm | 224 +++++++++++++----- 5 files changed, 183 insertions(+), 73 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index ccc85e9..726d8a7 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -38,9 +38,13 @@ scp /etc/chelon/certs/chelon_ca.crt ~/.chelon/certs/ ### Sign an RPM ```bash -# Sign a single RPM +# Sign a single RPM (detached signature) chelon-sign-rpm package.rpm +# Embed signature into RPM header (Integrated Signing) +# This allows 'rpm -K' to work natively +chelon-sign-rpm --resign package.rpm + # Specify key type chelon-sign-rpm --key-type legacy package.rpm diff --git a/server/auth.py b/server/auth.py index 420189a..d2696ff 100644 --- a/server/auth.py +++ b/server/auth.py @@ -151,7 +151,11 @@ 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: + logger.info(f"Token {token_id} not found in memory, reloading from {self.tokens_file}") + self.tokens = self._load_tokens() + if token_id not in self.tokens: raise ValueError(f"Unknown token: {token_id}") diff --git a/server/chelon-service.py b/server/chelon-service.py index ab4a785..887dfc0 100644 --- a/server/chelon-service.py +++ b/server/chelon-service.py @@ -357,16 +357,22 @@ 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', 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 + 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 index 0ea13ffa793ef4d9a6773b765e33f9a19211b4c8..065874a9f9752230256fac9f91177bd3c08ffb21 100644 GIT binary patch delta 2020 zcmZWpTTB~A6rI_%*K69Q?LTw?(Jb0Q`webtV3AUeMX#hZjn>4pWWFAR{qu&;`PDqbkTsY)~z! zgBnSb=QFEBveFT8sy#V9`C)gAfJix!|2_#E24~z1CxJtM|cCL+DLfes1?ZO#l30>53 z5f-9m$T^J+=&c}9l0EyEK5FN|4k}88{HTr*I78)H8g77+Fb{nK1+Bf#9)?AD8xpqA zI9!Bg^cnRV#`owtXdnBL9)~c0?2@v9#lxcZ6{@NpCzRQN;pr)`0;-jJmI#c*9+P3z zJR19Y=NJ87y0X)24O7BYY|{2aD|SLDLjhIIpmT;tiUZS<g%+v&466E(lz3>ysWo9#~ZbnSgM6XJ*GS+rHZIRPBiFo>{^e|XCn$^#V{LDk*dgb z?T4K%`M;2emJH_YokcPcQORy$b>&(kQf!Ome<5<$Qf!0`mB`^6V~!*Uc~OnYp?i+n zgee~+6>*|dCf3R(<${2mwYH)WgP>)TR$15yP?^!~ESKjS)^OAfgUckn&KWN@O(Vqd`Hf)kHKy^bi%+k}rBk*aEd^RIs2$vw+GhR_nJxgrE7UX!~$~0p;}aKv8Zul^MM=d zp%JrCzTSt8_>`^yv?vmwhAsdJ*tyYGJ0o6c^yIJ1;MX zeleS=&_2AT)5zth7_mwNRuP-lV_ueH@a;0MmLey>)XqJBn&db+MV~M>VF^Ib33q*+ z7>lQ+L|-D-BgMtO#6Vn34oYI*;83!+T0|W~81Z zjwO9glRG@GN@Ti;K$;~pI+Gcg&x@twShu@3-q$lUAa%zElT!Scly;y6>v7|}zNzhC zYePIX*c*Q-ogPf1qqbtGN5i&lT0Yj(O)~3F^+^5bvaN;1cLsR9UJ?Ckt5Id|dl!Z3 z>;uD(8NeIP17c^xx`js-`-oWp9hcO1LU9(c$Kb8Hd1XaAL35Ui_;%K%+-m{pHys4ai?2+cWOAATDrKF;`p zG#D?d*k7icDAr(mqKpdy^+dG-+mnDMNV}UI^kkvCDL_wA<&A!7Y9}PNzo5yZylLVI zx+u)t6d|!aE@B5n9Cb^C1U2UlQsrJ!>^r?K6VwMpZpeU$^<{RrC#& H65Ia)Fxj!@ delta 1661 zcmZuxZA=?w9Dkl(uP=AKqwU?b<+#4^($b2{3Y52ikU1pn0@&0J7uSvqwn5F5T8@xu z7|TBB5|`+F^aFm{5~C&<9TF2!quCtT^1)5BWMue7<0t(BzGP-b zIHNLgR%JEKn6Sk;mD4y(@M;ydg9*E8mq0B@(Qc43WgRh!WB@>QYN{ElmYAWU2`MYy zGj4F&u8LVH#daP6AbR2v02ELIvSKZWl`Vnl%DQn-TMU*3KhXAE)|HH+?;H~`q>LPb92_ z%+EUX2)oK^(6yZvTMFo9AtFU~G@U&;fl7j(vt&%Vl6**O%#z%Pz7cESeth~~Nw42J zVx6I847ex!MMuRZx1}C*Q(lmR0g$o%8zYLYSq0>g1RmekS4hEmCe7>$so3*u4fs!1 z(Vk}YG0NN0wqzT*^*L_mEYghrzdosXZb%6LKS;CPu(6VV0vDoS?f?ApoEHpH<&rZU zdci3kSJ#OrQwmn4IXrkqJ}_w0ra}37yaq+v1I59GJ$3!SD&E^7R=FG z)eVqGrRsgiC=Z%>EQLZLbVTkpYY)O-5WOj%Zv4#z-0~e)^jlZ7n7%oBbF?`68TE1h z7lSKuNJ#!lUKOTf1goHJ0MMCZTq9dKCJSR>&MilFE}qh>NlUjGun2(dWBp15qU4gU91HL%}oJ2PQeYGQ4 zEu17kjw50zv=ZJ$V3I%%HPi;+D0-~66V9Tuwf9XG`E1>OxUl?w-4av|6T{~)L`-FO zb(i}K4ZUyyebVqDt7Rc(g)?Y2*a2nqLD0|Ow~F>IxpxX}1Yd=VC>Qz#&ZA6YkUo!X z7txE2&qFVQVafgs-rr@WXU6h{sqvH3(?|{vOG6~!Q39lafg zt@Y^=CR-GQaN9^j4ECdQ<|}CG$Z$ PbzO-ZWT`J%8uNbumnKF= diff --git a/tools/chelon-sign-rpm b/tools/chelon-sign-rpm index 3f788c3..3eede80 100755 --- a/tools/chelon-sign-rpm +++ b/tools/chelon-sign-rpm @@ -3,40 +3,157 @@ 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. +Supports creating detached signatures (.asc) and embedding signatures via rpmsign integration. """ import os import sys import argparse +import subprocess +import tempfile +import base64 from pathlib import Path -from typing import Optional +from typing import Optional, List # Add tools directory to path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from chelon_client import get_client, ChelonClientError +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 sign_rpm(rpm_path: str, output_path: Optional[str] = None, - key_type: str = 'modern', verbose: bool = False) -> str: +def gpg_mode(args: List[str]): """ - Sign an RPM file (creates detached signature) + 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 = "-" + key_id = None + + # Simple argument parser for GPG flags + i = 0 + while i < len(args): + arg = args[i] + if arg in ('-o', '--output'): + output_file = args[i+1] + i += 2 + elif arg == '-sbo': + output_file = args[i+1] + i += 2 + elif arg == '-u': + key_id = args[i+1] + i += 2 + elif arg == '--': + if i + 1 < len(args): + input_file = args[i+1] + break + elif arg == '--version': + print("gpg (GnuPG) 2.4.5") + sys.exit(0) + else: + i += 1 + + # Read input data + if input_file == "-": + data = sys.stdin.buffer.read() + else: + 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') - 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 + # Initialize client + try: + client = get_client() + response = client.sign_data(data, key_type=key_type, operation='rpm') + signature = response['signature'] - Returns: - Path to signature file + # 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 != '-': + with open(output_file, 'w' if is_armored else 'wb') as f: + if is_armored: + f.write(signature) + else: + # Strip headers and decode if binary expected + # Simplified: just dearmor using gpg if available, or just write as is if armored + if signature.startswith('-----BEGIN'): + # Need to dearmor + proc = subprocess.Popen(['gpg', '--dearmor'], stdin=subprocess.PIPE, stdout=f) + proc.communicate(input=signature.encode('utf-8')) + else: + f.write(base64.b64decode(signature)) + else: + if is_armored: + sys.stdout.write(signature) + else: + sys.stdout.buffer.write(base64.b64decode(signature)) + + except Exception as e: + print(f"GPG Emulation Error: {e}", file=sys.stderr) + sys.exit(1) + + sys.exit(0) + + +def resign_rpm(rpm_path: str, key_type: str = 'modern', verbose: bool = False): """ - rpm_file = Path(rpm_path) + 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"Resigning RPM: {rpm_file}") + print(f"Using Key: {key_type} ({key_id})") + + # Set environment for the wrapper call + os.environ['CHELON_KEY_TYPE'] = key_type + + cmd = [ + 'rpmsign', + '--define', f'__gpg {script_path}', + '--define', f'_gpg_name {key_id}', + '--define', '_gpg_sign_cmd_extra_args --batch --no-tty', + '--resign', str(rpm_file) + ] + + try: + result = subprocess.run(cmd, check=True, capture_output=not verbose, text=True) + if verbose: + print("✓ Signature embedded successfully") + return True + except subprocess.CalledProcessError as e: + print(f"Error during rpmsign: {e.stderr if e.stderr else e}", file=sys.stderr) + return False + + +def sign_rpm_detached(rpm_path: str, output_path: Optional[str] = None, + key_type: str = 'modern', verbose: bool = False) -> str: + """ + Sign an RPM file (creates detached signature) + """ + rpm_file = Path(rpm_path) if not rpm_file.exists(): raise FileNotFoundError(f"RPM file not found: {rpm_path}") @@ -47,58 +164,40 @@ def sign_rpm(rpm_path: str, output_path: Optional[str] = None, 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) + f.write(response['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(): + # Detect if we are being called as a GPG wrapper + # rpmsign usually calls with a lot of flags, first one is often --no-verbose + if len(sys.argv) > 1 and sys.argv[1].startswith('--'): + # Check if it looks like a GPG call (batch, detach-sign, etc) + gpg_flags = {'--version', '--batch', '-sbo', '--detach-sign', '--armor', '-u'} + if any(arg in gpg_flags for arg in sys.argv): + gpg_mode(sys.argv[1:]) + parser = argparse.ArgumentParser( - description='Sign RPM packages using Chelon service (creates detached signatures)', + description='Sign RPM packages using Chelon service', 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('--resign', action='store_true', help='Embed signature into RPM header (requires rpmsign)') + 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') @@ -106,31 +205,28 @@ def main(): 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 - + if args.resign: + success = resign_rpm(args.rpm_file, key_type=args.key_type, verbose=args.verbose) + return 0 if success else 1 + else: + output_file = sign_rpm_detached( + 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 From c39f6eb0e8cdf01550fc6b8d250ed63eff9b557e Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 09:54:54 -0500 Subject: [PATCH 02/13] Code review changes Signed-off-by: Scott R. Shinn --- server/auth.py | 7 +- server/chelon-service.py | 1 + .../__pycache__/chelon_client.cpython-314.pyc | Bin 10683 -> 11091 bytes tools/chelon-sign-rpm | 138 ++++++++++++++---- tools/chelon_client.py | 19 ++- 5 files changed, 131 insertions(+), 34 deletions(-) diff --git a/server/auth.py b/server/auth.py index d2696ff..8cd25f4 100644 --- a/server/auth.py +++ b/server/auth.py @@ -153,8 +153,11 @@ def validate_token(self, token: str) -> Dict: # Check if token exists, reload if not found (to handle new tokens without restart) 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() + 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() if token_id not in self.tokens: raise ValueError(f"Unknown token: {token_id}") diff --git a/server/chelon-service.py b/server/chelon-service.py index 887dfc0..bce63be 100644 --- a/server/chelon-service.py +++ b/server/chelon-service.py @@ -369,6 +369,7 @@ def sign_repodata(): 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')) diff --git a/tools/__pycache__/chelon_client.cpython-314.pyc b/tools/__pycache__/chelon_client.cpython-314.pyc index 065874a9f9752230256fac9f91177bd3c08ffb21..cd9b840ee38e31659e3d5e59ee0dd989d43c1662 100644 GIT binary patch delta 999 zcmZ8gT})D87(SnR;9wjM27;i{5jhCrZ>HG-+pJ72`>{>)zY}1x5ZIBGTK;Tq4y(<% zPv^X_@TwbYGrFqHZd}KHu~HU3_2eZ??!RGBO?G z!$kGCHgZ>Fv-p)V{2n;K5bmgS3j=fg@l=%TXVFWFMKX>H?;YZ+Ng3S7bt!s01Vn29C_?Mvd?1;@upF!fyDNv~VugY%I)g?YhfkE$Wv6s;LeHR1 z=|qbNt`P>pQ7AMOqr~b^qNMeqN1Eu{>hq9EKJJkvcvDuVI>DRe{ERHPpQ(gil@yvK zH}MQ!(I%Azi5EZ1D>)|$2}{xmpbTA1Z%Vn4VefOgy`CPs+vW85OYEF~_&@21b7+`M zxLD+X6Vf9ffum%hDMqkve~;bC`D0?lcyml7NbG=w-_VGHrfK+*ECM@lm;7!DTWTWN zHG#A(A}gxZy)9lS4j0-YT3aCPH5FB7&yUQE1ji!kD!3-ojP0EWFCyxSKz&rDo$sIN z54t0&^MM=DOwD}POjqzhB(peBw@0a`$EL=%DSCsVgRSqnm%6u^$_=J6ns3>X(^0*7 zmWrNZ=A^$=1>wT_Z_4e4w)KX#aD98&)Um1R49h$BD8+FLW}RZTse%nEq@=O|xE(5! ze-#63D6XCcpaG6+?#M6m$8lB!Dg?D~SM!NC&T89IfffGF)c_kLbT;rZRHqwE(F#s4 z?;IveS)Z%)X?f{@>#?VlYk*POQrm%gxDgOMm9KnqPDhV}^|LXt zj~#M*SqCR<(hINWRfCssH19}`#t0Rh4Y3+X>lm;c`k-F{hIXV3@?aJn&v=f6WJL`0 za#lEDr~=FIv*EqG8odg8p&D)+K4TiC=&u|KU+b>}{$%|8rL{&QScNX*A((`n1q?pL zyI0|4!7EWCs`Ep&>6W$$(PjiJ0(2>-KrjVIOnT4Pa_U< H&2RnzpP3Z_ delta 664 zcmcZ{wmXj-pDtF)n1h~kntk}h`cE#H?w$Z?gwTN=b?ncLgBf* zA6UWiYz#6g3!UfMeqaZ4IT+;B7N*RN`^c#}`5~(ulM%~iMz-^ei~^f4vMV!Ae!(Tc z${WTIATjwbm%?O8E^k&oAWsU+>*UI3ST%B+K>hyRAS^z_<^wF@e) zit7XUJP-JVuk*`aa>cMZMh8hXJc?1D=8Wub@* zJP|iG&lb>OWOUwqLU2Djqvhrf$$mygx5*OH{sI<2cNAHI2r&?0Ke=3b3s9ksOeC{3 zNca{29}tH}%GZ5it~-;=Lkk^<|_0jjOy)lV$Q)GssC zFV0NQ(=T$HT%e?_7y&Xj6GU(T2~Cb7V-OpzsVH~y9wliuusf0`-&L|;te-5V{7V32 zU{NlJumTa*lND4{85eGLQ`yL<5)Kk61rnNU5Ys9^0&pF-7}JU@CrhiDF)o}Oq_$ST z6eJ6F0Z30#?PPv+FCI|jegdb3?|ze0)k_$yCf`z5W86HMQR5S1^W-lYs;q55)te{F zX?C-@gY*?nUa#pR@~ diff --git a/tools/chelon-sign-rpm b/tools/chelon-sign-rpm index 3eede80..c2c3df6 100755 --- a/tools/chelon-sign-rpm +++ b/tools/chelon-sign-rpm @@ -10,7 +10,6 @@ import os import sys import argparse import subprocess -import tempfile import base64 from pathlib import Path from typing import Optional, List @@ -29,6 +28,21 @@ except ImportError: 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): + pass + return "2.4.4" + + def gpg_mode(args: List[str]): """ Emulate GPG behavior when called by rpmsign. @@ -37,35 +51,57 @@ def gpg_mode(args: List[str]): """ output_file = None input_file = "-" - key_id = None # Simple argument parser for GPG flags i = 0 while i < len(args): arg = args[i] if arg in ('-o', '--output'): - output_file = args[i+1] - i += 2 + 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': - output_file = args[i+1] - i += 2 + 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': - key_id = args[i+1] - i += 2 + 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': - print("gpg (GnuPG) 2.4.5") + version = get_gpg_version() + print(f"gpg (GnuPG) {version}") sys.exit(0) else: i += 1 - # Read input data + # Read input data with a 10MB limit (DoS protection) + MAX_INPUT_SIZE = 10 * 1024 * 1024 # 10MB if input_file == "-": - data = sys.stdin.buffer.read() + data = sys.stdin.buffer.read(MAX_INPUT_SIZE + 1) + if len(data) > MAX_INPUT_SIZE: + print(f"Error: Input data from stdin exceeds limit of {MAX_INPUT_SIZE} bytes", file=sys.stderr) + sys.exit(1) 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() @@ -84,23 +120,58 @@ def gpg_mode(args: List[str]): is_armored = '--armor' in args or '-a' in args if output_file and output_file != '-': - with open(output_file, 'w' if is_armored else 'wb') as f: + try: if is_armored: - f.write(signature) + with open(output_file, 'w') as f: + f.write(signature) else: # Strip headers and decode if binary expected - # Simplified: just dearmor using gpg if available, or just write as is if armored if signature.startswith('-----BEGIN'): # Need to dearmor - proc = subprocess.Popen(['gpg', '--dearmor'], stdin=subprocess.PIPE, stdout=f) - proc.communicate(input=signature.encode('utf-8')) + 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: - f.write(base64.b64decode(signature)) + with open(output_file, 'wb') as f: + f.write(base64.b64decode(signature)) + 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: - sys.stdout.buffer.write(base64.b64decode(signature)) + 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: + sys.stdout.buffer.write(base64.b64decode(signature)) + sys.stdout.buffer.flush() except Exception as e: print(f"GPG Emulation Error: {e}", file=sys.stderr) @@ -109,7 +180,7 @@ def gpg_mode(args: List[str]): sys.exit(0) -def resign_rpm(rpm_path: str, key_type: str = 'modern', verbose: bool = False): +def sign_rpm_integrated(rpm_path: str, key_type: str = 'modern', verbose: bool = False): """ Embed signature into RPM using rpmsign and self as wrapper. """ @@ -124,11 +195,12 @@ def resign_rpm(rpm_path: str, key_type: str = 'modern', verbose: bool = False): key_id = key_ids.get(key_type, key_type) if verbose: - print(f"Resigning RPM: {rpm_file}") + print(f"Integrated Signing: {rpm_file}") print(f"Using Key: {key_type} ({key_id})") - # Set environment for the wrapper call - os.environ['CHELON_KEY_TYPE'] = key_type + # Prepare environment for the wrapper call + new_env = os.environ.copy() + new_env['CHELON_KEY_TYPE'] = key_type cmd = [ 'rpmsign', @@ -139,12 +211,18 @@ def resign_rpm(rpm_path: str, key_type: str = 'modern', verbose: bool = False): ] try: - result = subprocess.run(cmd, check=True, capture_output=not verbose, text=True) + 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: - print(f"Error during rpmsign: {e.stderr if e.stderr else e}", file=sys.stderr) + 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 @@ -183,12 +261,15 @@ def sign_rpm_detached(rpm_path: str, output_path: Optional[str] = None, def main(): # Detect if we are being called as a GPG wrapper - # rpmsign usually calls with a lot of flags, first one is often --no-verbose + # rpmsign usually calls with a lot of flags, including -sbo or --detach-sign if len(sys.argv) > 1 and sys.argv[1].startswith('--'): - # Check if it looks like a GPG call (batch, detach-sign, etc) - gpg_flags = {'--version', '--batch', '-sbo', '--detach-sign', '--armor', '-u'} + # 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 '--version' in sys.argv and len(sys.argv) == 2: + # Handle standalone --version for discovery tools + gpg_mode(sys.argv[1:]) parser = argparse.ArgumentParser( description='Sign RPM packages using Chelon service', @@ -210,7 +291,7 @@ def main(): try: if args.resign: - success = resign_rpm(args.rpm_file, key_type=args.key_type, verbose=args.verbose) + success = sign_rpm_integrated(args.rpm_file, key_type=args.key_type, verbose=args.verbose) return 0 if success else 1 else: output_file = sign_rpm_detached( @@ -224,6 +305,7 @@ def main(): return 0 except KeyboardInterrupt: + print("\nInterrupted by user", file=sys.stderr) return 130 except Exception as e: print(f"Error: {e}", file=sys.stderr) diff --git a/tools/chelon_client.py b/tools/chelon_client.py index eab43ea..87846d8 100644 --- a/tools/chelon_client.py +++ b/tools/chelon_client.py @@ -53,12 +53,23 @@ 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 + 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(): From ef9d4a41d79dffd8359ad14d5f0fb61c6b4a3945 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 09:55:32 -0500 Subject: [PATCH 03/13] Get rid of cache dirs Signed-off-by: Scott R. Shinn --- server/__pycache__/audit.cpython-314.pyc | Bin 5464 -> 0 bytes server/__pycache__/auth.cpython-314.pyc | Bin 10012 -> 0 bytes .../__pycache__/chelon-service.cpython-314.pyc | Bin 11363 -> 0 bytes .../__pycache__/signing_engine.cpython-314.pyc | Bin 4569 -> 0 bytes tools/__pycache__/chelon_client.cpython-314.pyc | Bin 11091 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 server/__pycache__/audit.cpython-314.pyc delete mode 100644 server/__pycache__/auth.cpython-314.pyc delete mode 100644 server/__pycache__/chelon-service.cpython-314.pyc delete mode 100644 server/__pycache__/signing_engine.cpython-314.pyc delete mode 100644 tools/__pycache__/chelon_client.cpython-314.pyc diff --git a/server/__pycache__/audit.cpython-314.pyc b/server/__pycache__/audit.cpython-314.pyc deleted file mode 100644 index 3224717211ca4d321eaf7236a883bf8df53b67da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5464 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 diff --git a/server/__pycache__/chelon-service.cpython-314.pyc b/server/__pycache__/chelon-service.cpython-314.pyc deleted file mode 100644 index 5ba1820cfedf7ddc31bacd792004012efe624bd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11363 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 Date: Thu, 8 Jan 2026 09:57:05 -0500 Subject: [PATCH 04/13] added Signed-off-by: Scott R. Shinn --- .gitignore | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .gitignore 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 From e333e5701dc252631e57ab86a3ce4ad1636db360 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:11:35 -0500 Subject: [PATCH 05/13] Update server/chelon-service.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- server/chelon-service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/chelon-service.py b/server/chelon-service.py index bce63be..9087859 100644 --- a/server/chelon-service.py +++ b/server/chelon-service.py @@ -359,7 +359,7 @@ def sign_repodata(): # Run the Flask app # 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', 5050)) + port = int(config.get('CHELON_PORT') or os.environ.get('CHELON_PORT') or 5050) logger.info(f"Starting Chelon service on {host}:{port}") From b00bf0a9bc861283250d039d6bdd365b8fda14a2 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:12:03 -0500 Subject: [PATCH 06/13] Update server/auth.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- server/auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/auth.py b/server/auth.py index 8cd25f4..985126a 100644 --- a/server/auth.py +++ b/server/auth.py @@ -158,7 +158,10 @@ def validate_token(self, token: str) -> Dict: 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}") + if token_id not in self.tokens: raise ValueError(f"Unknown token: {token_id}") From 2b4f5e756cae5f0931087c0cc4fcf3714b610eff Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:28:47 -0500 Subject: [PATCH 07/13] Consolidate signing tools and address PR #2 feedback - Unified 'chelon-sign-rpm' and 'chelon-sign-repomd' into 'chelon-sign' - Updated spec file to reflect tool consolidation (v1.0.0-3) - Security: Sanitized script path in RPM macros - Security: Optimized DoS protection with chunked reading - Fix: Robust error handling for base64 decoding - Fix: Improved client certificate fallback logic --- chelon.spec | 13 ++- tools/{chelon-sign-rpm => chelon-sign} | 113 ++++++++++++++++------ tools/chelon-sign-repomd | 127 ------------------------- tools/chelon_client.py | 3 +- 4 files changed, 93 insertions(+), 163 deletions(-) rename tools/{chelon-sign-rpm => chelon-sign} (73%) delete mode 100755 tools/chelon-sign-repomd diff --git a/chelon.spec b/chelon.spec index beb73b3..705e51b 100644 --- a/chelon.spec +++ b/chelon.spec @@ -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/tools/chelon-sign-rpm b/tools/chelon-sign similarity index 73% rename from tools/chelon-sign-rpm rename to tools/chelon-sign index c2c3df6..0978690 100755 --- a/tools/chelon-sign-rpm +++ b/tools/chelon-sign @@ -1,9 +1,12 @@ #!/usr/bin/env python3 """ -Chelon Sign RPM +Chelon Sign (Unified Client) -Sign RPM packages using Chelon service. -Supports creating detached signatures (.asc) and embedding signatures via rpmsign integration. +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 @@ -11,6 +14,8 @@ import sys import argparse import subprocess import base64 +import shlex +import binascii from pathlib import Path from typing import Optional, List @@ -93,10 +98,28 @@ def gpg_mode(args: List[str]): # Read input data with a 10MB limit (DoS protection) MAX_INPUT_SIZE = 10 * 1024 * 1024 # 10MB if input_file == "-": - data = sys.stdin.buffer.read(MAX_INPUT_SIZE + 1) - if len(data) > MAX_INPUT_SIZE: - print(f"Error: Input data from stdin exceeds limit of {MAX_INPUT_SIZE} bytes", file=sys.stderr) - sys.exit(1) + # 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: @@ -145,7 +168,11 @@ def gpg_mode(args: List[str]): sys.exit(1) else: with open(output_file, 'wb') as f: - f.write(base64.b64decode(signature)) + 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) @@ -170,7 +197,11 @@ def gpg_mode(args: List[str]): print("Error: 'gpg' command not found.", file=sys.stderr) sys.exit(1) else: - sys.stdout.buffer.write(base64.b64decode(signature)) + 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 Exception as e: @@ -204,7 +235,7 @@ def sign_rpm_integrated(rpm_path: str, key_type: str = 'modern', verbose: bool = cmd = [ 'rpmsign', - '--define', f'__gpg {script_path}', + '--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) @@ -226,32 +257,38 @@ def sign_rpm_integrated(rpm_path: str, key_type: str = 'modern', verbose: bool = return False -def sign_rpm_detached(rpm_path: str, output_path: Optional[str] = None, - key_type: str = 'modern', verbose: bool = False) -> str: +def sign_file_detached(file_path: str, output_path: Optional[str] = None, + key_type: str = 'modern', operation: str = 'rpm', verbose: bool = False) -> str: """ - Sign an RPM file (creates detached signature) + Sign a file (creates detached signature) + Supports both rpm and repodata operations """ - rpm_file = Path(rpm_path) - if not rpm_file.exists(): - raise FileNotFoundError(f"RPM file not found: {rpm_path}") + 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(rpm_file) + '.asc' + output_path = str(target_file) + '.asc' if verbose: - print(f"Signing: {rpm_path}") + 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(rpm_file), key_type=key_type, operation='rpm') + response = client.sign_file(str(target_file), key_type=key_type, operation=operation) with open(output_path, 'w') as f: f.write(response['signature']) if verbose: - print(f"✓ Signature written to: {output_path}") + 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: @@ -261,24 +298,25 @@ def sign_rpm_detached(rpm_path: str, output_path: Optional[str] = None, def main(): # Detect if we are being called as a GPG wrapper - # rpmsign usually calls with a lot of flags, including -sbo or --detach-sign - if len(sys.argv) > 1 and sys.argv[1].startswith('--'): + 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 '--version' in sys.argv and len(sys.argv) == 2: + 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='Sign RPM packages using Chelon service', + description='Chelon Sign - Unified Signing Tool', epilog='Environment variables: CHELON_URL, CHELON_TOKEN, CHELON_CERT_DIR' ) - parser.add_argument('rpm_file', help='Path to RPM file') - parser.add_argument('--resign', action='store_true', help='Embed signature into RPM header (requires rpmsign)') - parser.add_argument('-o', '--output', help='Output signature file (default: .asc, only for detached)') + 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') @@ -288,16 +326,30 @@ def main(): 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: - success = sign_rpm_integrated(args.rpm_file, key_type=args.key_type, verbose=args.verbose) + 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_rpm_detached( - args.rpm_file, + 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: @@ -314,4 +366,3 @@ def main(): 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_client.py b/tools/chelon_client.py index 87846d8..6d0c993 100644 --- a/tools/chelon_client.py +++ b/tools/chelon_client.py @@ -66,7 +66,8 @@ def __init__(self, if alt_cert.exists() and alt_key.exists(): self.client_cert = alt_cert self.client_key = alt_key - self.ca_cert = alt_ca + if alt_ca.exists(): + self.ca_cert = alt_ca else: raise ChelonClientError(f"Client certificate not found: {self.client_cert}") From 563f2638e59947e06d4b18d697b49c8f60c85c15 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:41:15 -0500 Subject: [PATCH 08/13] Update server/auth.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- server/auth.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/auth.py b/server/auth.py index 985126a..c3cda92 100644 --- a/server/auth.py +++ b/server/auth.py @@ -162,9 +162,6 @@ def validate_token(self, token: str) -> Dict: if token_id not in self.tokens: raise ValueError(f"Unknown token after reload: {token_id}") - if token_id not in self.tokens: - raise ValueError(f"Unknown token: {token_id}") - token_info = self.tokens[token_id] # Verify secret From c82853940a5ad81ade43309e22179a4175a7972d Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:41:36 -0500 Subject: [PATCH 09/13] Update tools/chelon-sign Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/chelon-sign | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/chelon-sign b/tools/chelon-sign index 0978690..b7b3013 100755 --- a/tools/chelon-sign +++ b/tools/chelon-sign @@ -204,6 +204,9 @@ def gpg_mode(args: List[str]): 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) From 6b606b7695620f2d47c09304076a1afcbf5b3334 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:42:50 -0500 Subject: [PATCH 10/13] Update tools/chelon-sign Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/chelon-sign | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/chelon-sign b/tools/chelon-sign index b7b3013..8400ae7 100755 --- a/tools/chelon-sign +++ b/tools/chelon-sign @@ -283,8 +283,12 @@ def sign_file_detached(file_path: str, output_path: Optional[str] = None, client = get_client() response = client.sign_file(str(target_file), key_type=key_type, operation=operation) - with open(output_path, 'w') as f: - f.write(response['signature']) + 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") From 8554882ece74bfabb73d7752715152ff0a98ef0d Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:43:37 -0500 Subject: [PATCH 11/13] Update tools/chelon-sign Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/chelon-sign | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/chelon-sign b/tools/chelon-sign index 8400ae7..6e8fe00 100755 --- a/tools/chelon-sign +++ b/tools/chelon-sign @@ -44,6 +44,7 @@ def get_gpg_version() -> str: 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" From a1177ef58416d973d10ebbf9c942254c2db3d5f9 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:45:18 -0500 Subject: [PATCH 12/13] Update documentation and finalize consolidation - Updated docs/USAGE.md to reflect tool consolidation (chelon-sign-rpm/repomd -> chelon-sign) - Minor cleanup in auth.py (remove redundant token check) - Added specific error handling for ChelonClientError in GPG emulation mode --- docs/USAGE.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index 726d8a7..3f20383 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -39,18 +39,18 @@ scp /etc/chelon/certs/chelon_ca.crt ~/.chelon/certs/ ```bash # Sign a single RPM (detached signature) -chelon-sign-rpm package.rpm +chelon-sign package.rpm # Embed signature into RPM header (Integrated Signing) # This allows 'rpm -K' to work natively -chelon-sign-rpm --resign package.rpm +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 ``` @@ -75,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:** @@ -226,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 @@ -241,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 ``` @@ -261,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" @@ -312,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 ``` --- @@ -422,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 @@ -430,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 ``` From e959cd234ebf65ed032bfcd80d6e44759bb709fc Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Thu, 8 Jan 2026 10:54:31 -0500 Subject: [PATCH 13/13] Bump Release to 3 in spec file --- chelon.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chelon.spec b/chelon.spec index 705e51b..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