33Chelon Sign RPM
44
55Sign RPM packages using Chelon service.
6- Creates detached GPG signatures (.asc files).
7-
8- Note: This creates detached signatures. For embedded RPM signatures,
9- you'll need to use rpm-sign library or rpmsign command with proper integration.
6+ Supports creating detached signatures (.asc) and embedding signatures via rpmsign integration.
107"""
118
129import os
1310import sys
1411import argparse
12+ import subprocess
13+ import tempfile
14+ import base64
1515from pathlib import Path
16- from typing import Optional
16+ from typing import Optional , List
1717
1818# Add tools directory to path
1919sys .path .insert (0 , os .path .dirname (os .path .abspath (__file__ )))
2020
21- from chelon_client import get_client , ChelonClientError
21+ try :
22+ from chelon_client import get_client , ChelonClientError
23+ except ImportError :
24+ # Handle direct execution if not in path
25+ try :
26+ from tools .chelon_client import get_client , ChelonClientError
27+ except ImportError :
28+ print ("Error: Could not import chelon_client. Ensure tools/ is in PYTHONPATH." , file = sys .stderr )
29+ sys .exit (1 )
2230
2331
24- def sign_rpm (rpm_path : str , output_path : Optional [str ] = None ,
25- key_type : str = 'modern' , verbose : bool = False ) -> str :
32+ def gpg_mode (args : List [str ]):
2633 """
27- Sign an RPM file (creates detached signature)
34+ Emulate GPG behavior when called by rpmsign.
35+ rpmsign calls gpg with flags like:
36+ --no-verbose --no-armor --batch --no-tty -u <KEYID> -sbo <SIGFILE> -- -
37+ """
38+ output_file = None
39+ input_file = "-"
40+ key_id = None
41+
42+ # Simple argument parser for GPG flags
43+ i = 0
44+ while i < len (args ):
45+ arg = args [i ]
46+ if arg in ('-o' , '--output' ):
47+ output_file = args [i + 1 ]
48+ i += 2
49+ elif arg == '-sbo' :
50+ output_file = args [i + 1 ]
51+ i += 2
52+ elif arg == '-u' :
53+ key_id = args [i + 1 ]
54+ i += 2
55+ elif arg == '--' :
56+ if i + 1 < len (args ):
57+ input_file = args [i + 1 ]
58+ break
59+ elif arg == '--version' :
60+ print ("gpg (GnuPG) 2.4.5" )
61+ sys .exit (0 )
62+ else :
63+ i += 1
64+
65+ # Read input data
66+ if input_file == "-" :
67+ data = sys .stdin .buffer .read ()
68+ else :
69+ with open (input_file , 'rb' ) as f :
70+ data = f .read ()
71+
72+ # Determine key type from key_id (mapping logic)
73+ # In this wrapper, we rely on env CHELON_KEY_TYPE or default to modern
74+ key_type = os .environ .get ('CHELON_KEY_TYPE' , 'modern' )
2875
29- Args:
30- rpm_path: Path to RPM file
31- output_path: Path for signature file (default: rpm_path + '.asc' )
32- key_type: GPG key type to use
33- verbose: Print verbose output
76+ # Initialize client
77+ try :
78+ client = get_client ( )
79+ response = client . sign_data ( data , key_type = key_type , operation = 'rpm' )
80+ signature = response [ 'signature' ]
3481
35- Returns:
36- Path to signature file
82+ # rpmsign usually expects binary signature if --armor is not passed
83+ # chelon-gpg-wrapper.sh handles this by checking for --armor
84+ is_armored = '--armor' in args or '-a' in args
85+
86+ if output_file and output_file != '-' :
87+ with open (output_file , 'w' if is_armored else 'wb' ) as f :
88+ if is_armored :
89+ f .write (signature )
90+ else :
91+ # Strip headers and decode if binary expected
92+ # Simplified: just dearmor using gpg if available, or just write as is if armored
93+ if signature .startswith ('-----BEGIN' ):
94+ # Need to dearmor
95+ proc = subprocess .Popen (['gpg' , '--dearmor' ], stdin = subprocess .PIPE , stdout = f )
96+ proc .communicate (input = signature .encode ('utf-8' ))
97+ else :
98+ f .write (base64 .b64decode (signature ))
99+ else :
100+ if is_armored :
101+ sys .stdout .write (signature )
102+ else :
103+ sys .stdout .buffer .write (base64 .b64decode (signature ))
104+
105+ except Exception as e :
106+ print (f"GPG Emulation Error: { e } " , file = sys .stderr )
107+ sys .exit (1 )
108+
109+ sys .exit (0 )
110+
111+
112+ def resign_rpm (rpm_path : str , key_type : str = 'modern' , verbose : bool = False ):
37113 """
38- rpm_file = Path (rpm_path )
114+ Embed signature into RPM using rpmsign and self as wrapper.
115+ """
116+ rpm_file = Path (rpm_path ).absolute ()
117+ script_path = Path (__file__ ).absolute ()
118+
119+ # Map key_type to standard Atomicorp Key IDs if not provided
120+ key_ids = {
121+ 'legacy' : '4520AFA9' ,
122+ 'modern' : 'CB2C73F04F3BE076'
123+ }
124+ key_id = key_ids .get (key_type , key_type )
39125
126+ if verbose :
127+ print (f"Resigning RPM: { rpm_file } " )
128+ print (f"Using Key: { key_type } ({ key_id } )" )
129+
130+ # Set environment for the wrapper call
131+ os .environ ['CHELON_KEY_TYPE' ] = key_type
132+
133+ cmd = [
134+ 'rpmsign' ,
135+ '--define' , f'__gpg { script_path } ' ,
136+ '--define' , f'_gpg_name { key_id } ' ,
137+ '--define' , '_gpg_sign_cmd_extra_args --batch --no-tty' ,
138+ '--resign' , str (rpm_file )
139+ ]
140+
141+ try :
142+ result = subprocess .run (cmd , check = True , capture_output = not verbose , text = True )
143+ if verbose :
144+ print ("✓ Signature embedded successfully" )
145+ return True
146+ except subprocess .CalledProcessError as e :
147+ print (f"Error during rpmsign: { e .stderr if e .stderr else e } " , file = sys .stderr )
148+ return False
149+
150+
151+ def sign_rpm_detached (rpm_path : str , output_path : Optional [str ] = None ,
152+ key_type : str = 'modern' , verbose : bool = False ) -> str :
153+ """
154+ Sign an RPM file (creates detached signature)
155+ """
156+ rpm_file = Path (rpm_path )
40157 if not rpm_file .exists ():
41158 raise FileNotFoundError (f"RPM file not found: { rpm_path } " )
42159
@@ -47,90 +164,69 @@ def sign_rpm(rpm_path: str, output_path: Optional[str] = None,
47164 print (f"Signing: { rpm_path } " )
48165 print (f"Output: { output_path } " )
49166 print (f"Key type: { key_type } " )
50- print (f"Note: Creating detached signature (not embedding in RPM)" )
51167
52- # Get client
53168 try :
54169 client = get_client ()
55- except ChelonClientError as e :
56- print (f"Error initializing client: { e } " , file = sys .stderr )
57- sys .exit (1 )
58-
59- # Sign the file
60- try :
61- if verbose :
62- file_size = rpm_file .stat ().st_size
63- print (f"RPM size: { file_size :,} bytes" )
64- print ("Sending signing request..." )
65-
66170 response = client .sign_file (str (rpm_file ), key_type = key_type , operation = 'rpm' )
67171
68- if verbose :
69- print (f"✓ Signed successfully" )
70- print (f" Request ID: { response .get ('request_id' )} " )
71- print (f" Key ID: { response .get ('key_id' )} " )
72- print (f" Key Fingerprint: { response .get ('key_fingerprint' )} " )
73-
74- # Write signature to file
75- signature = response ['signature' ]
76172 with open (output_path , 'w' ) as f :
77- f .write (signature )
173+ f .write (response [ ' signature' ] )
78174
79175 if verbose :
80176 print (f"✓ Signature written to: { output_path } " )
81- print ()
82- print ("To verify: gpg --verify" , output_path , rpm_path )
83-
84177 return output_path
85178
86179 except ChelonClientError as e :
87180 print (f"Error signing file: { e } " , file = sys .stderr )
88181 sys .exit (1 )
89- except Exception as e :
90- print (f"Unexpected error: { e } " , file = sys .stderr )
91- sys .exit (1 )
92182
93183
94184def main ():
185+ # Detect if we are being called as a GPG wrapper
186+ # rpmsign usually calls with a lot of flags, first one is often --no-verbose
187+ if len (sys .argv ) > 1 and sys .argv [1 ].startswith ('--' ):
188+ # Check if it looks like a GPG call (batch, detach-sign, etc)
189+ gpg_flags = {'--version' , '--batch' , '-sbo' , '--detach-sign' , '--armor' , '-u' }
190+ if any (arg in gpg_flags for arg in sys .argv ):
191+ gpg_mode (sys .argv [1 :])
192+
95193 parser = argparse .ArgumentParser (
96- description = 'Sign RPM packages using Chelon service (creates detached signatures) ' ,
194+ description = 'Sign RPM packages using Chelon service' ,
97195 epilog = 'Environment variables: CHELON_URL, CHELON_TOKEN, CHELON_CERT_DIR'
98196 )
99197
100198 parser .add_argument ('rpm_file' , help = 'Path to RPM file' )
101- parser .add_argument ('-o' , '--output' , help = 'Output signature file (default: <rpm_file>.asc)' )
199+ parser .add_argument ('--resign' , action = 'store_true' , help = 'Embed signature into RPM header (requires rpmsign)' )
200+ parser .add_argument ('-o' , '--output' , help = 'Output signature file (default: <rpm_file>.asc, only for detached)' )
102201 parser .add_argument ('-k' , '--key-type' , choices = ['legacy' , 'modern' ], default = 'modern' ,
103202 help = 'GPG key type to use (default: modern)' )
104203 parser .add_argument ('--insecure' , action = 'store_true' , help = 'Disable SSL certificate verification' )
105204 parser .add_argument ('-v' , '--verbose' , action = 'store_true' , help = 'Verbose output' )
106205
107206 args = parser .parse_args ()
108207
109- # Set verify_ssl based on --insecure flag
110208 if args .insecure :
111209 os .environ ['CHELON_VERIFY_SSL' ] = 'false'
112210
113211 try :
114- output_file = sign_rpm (
115- args .rpm_file ,
116- output_path = args .output ,
117- key_type = args .key_type ,
118- verbose = args .verbose
119- )
120-
121- if not args .verbose :
122- print (output_file )
123-
124- return 0
125-
212+ if args .resign :
213+ success = resign_rpm (args .rpm_file , key_type = args .key_type , verbose = args .verbose )
214+ return 0 if success else 1
215+ else :
216+ output_file = sign_rpm_detached (
217+ args .rpm_file ,
218+ output_path = args .output ,
219+ key_type = args .key_type ,
220+ verbose = args .verbose
221+ )
222+ if not args .verbose :
223+ print (output_file )
224+ return 0
225+
126226 except KeyboardInterrupt :
127- print ("\n Interrupted" , file = sys .stderr )
128227 return 130
129228 except Exception as e :
130229 print (f"Error: { e } " , file = sys .stderr )
131- if args .verbose :
132- import traceback
133- traceback .print_exc ()
134230 return 1
135231
136232
0 commit comments