Skip to content

Commit e4d6179

Browse files
committed
Fixes for SSL support and signing workflow
Signed-off-by: Scott R. Shinn <scott@atomicorp.com>
1 parent 0f08f13 commit e4d6179

5 files changed

Lines changed: 183 additions & 73 deletions

File tree

docs/USAGE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ scp /etc/chelon/certs/chelon_ca.crt ~/.chelon/certs/
3838
### Sign an RPM
3939

4040
```bash
41-
# Sign a single RPM
41+
# Sign a single RPM (detached signature)
4242
chelon-sign-rpm package.rpm
4343

44+
# Embed signature into RPM header (Integrated Signing)
45+
# This allows 'rpm -K' to work natively
46+
chelon-sign-rpm --resign package.rpm
47+
4448
# Specify key type
4549
chelon-sign-rpm --key-type legacy package.rpm
4650

server/auth.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,11 @@ def validate_token(self, token: str) -> Dict:
151151

152152
token_id, secret = token.split(':', 1)
153153

154-
# Check if token exists
154+
# Check if token exists, reload if not found (to handle new tokens without restart)
155+
if token_id not in self.tokens:
156+
logger.info(f"Token {token_id} not found in memory, reloading from {self.tokens_file}")
157+
self.tokens = self._load_tokens()
158+
155159
if token_id not in self.tokens:
156160
raise ValueError(f"Unknown token: {token_id}")
157161

server/chelon-service.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -357,16 +357,22 @@ def sign_repodata():
357357

358358
if __name__ == '__main__':
359359
# Run the Flask app
360-
host = os.environ.get('CHELON_HOST', '127.0.0.1')
361-
port = int(os.environ.get('CHELON_PORT', 5050))
360+
# Prioritize config file over environment variables
361+
host = config.get('CHELON_HOST') or os.environ.get('CHELON_HOST', '127.0.0.1')
362+
port = int(config.get('CHELON_PORT') or os.environ.get('CHELON_PORT', 5050))
362363

363364
logger.info(f"Starting Chelon service on {host}:{port}")
364365

365-
# SSL/TLS Configuration
366-
ssl_cert = os.environ.get('CHELON_SSL_CERT')
367-
ssl_key = os.environ.get('CHELON_SSL_KEY')
368-
ssl_ca = os.environ.get('CHELON_SSL_CA')
369-
verify_client = os.environ.get('CHELON_VERIFY_CLIENT', 'false').lower() == 'true'
366+
# SSL/TLS Configuration - Prefer config file, fall back to environment
367+
ssl_cert = config.get('CHELON_SSL_CERT') or os.environ.get('CHELON_SSL_CERT')
368+
ssl_key = config.get('CHELON_SSL_KEY') or os.environ.get('CHELON_SSL_KEY')
369+
ssl_ca = config.get('CHELON_SSL_CA') or os.environ.get('CHELON_SSL_CA')
370+
371+
# Support both names for backward compatibility/consistency
372+
verify_client_val = (config.get('CHELON_SSL_VERIFY_CLIENT') or
373+
config.get('CHELON_VERIFY_CLIENT') or
374+
os.environ.get('CHELON_VERIFY_CLIENT', 'false'))
375+
verify_client = str(verify_client_val).lower() == 'true'
370376

371377
ssl_context = None
372378
if ssl_cert and ssl_key:
445 Bytes
Binary file not shown.

tools/chelon-sign-rpm

Lines changed: 160 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,157 @@
33
Chelon Sign RPM
44
55
Sign 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

129
import os
1310
import sys
1411
import argparse
12+
import subprocess
13+
import tempfile
14+
import base64
1515
from pathlib import Path
16-
from typing import Optional
16+
from typing import Optional, List
1717

1818
# Add tools directory to path
1919
sys.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

94184
def 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("\nInterrupted", 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

Comments
 (0)