Skip to content

Commit 7e3bebd

Browse files
committed
fix(signing): enforce binary data signing for valid RPM signatures
- Update signing_engine.py to sign bytes instead of hash strings - Update oracle-service.py to require base64 'data' field - Fixes invalid RPM signature generation - Adds verification test script Signed-off-by: Scott R. Shinn <scott@atomicorp.com>
1 parent 59b2cb6 commit 7e3bebd

5 files changed

Lines changed: 83 additions & 13 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
NAME = chelon
22
VERSION = 1.0.0
3-
RELEASE = 1
3+
RELEASE = 2
44

55
.PHONY: all clean srpm rpm
66

chelon.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Name: chelon
22
Version: 1.0.0
3-
Release: 1%{?dist}
3+
Release: 2%{?dist}
44
Summary: Remote GPG package signing service (Chelon)
55

66
License: GPL-2.0-or-later

server/oracle-service.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,10 @@ def _handle_signing(operation):
101101
data_id = f"raw_data:{len(sign_target)}b"
102102
except Exception as e:
103103
return jsonify({'error': f'Invalid Base64 data: {e}'}), 400
104-
elif package_hash:
105-
sign_target = package_hash
106-
data_id = package_hash
107-
elif repodata_hash:
108-
sign_target = repodata_hash
109-
data_id = repodata_hash
104+
elif package_hash or repodata_hash:
105+
return jsonify({'error': 'Signing by hash is deprecated. Please provide base64 encoded "data".'}), 400
110106
else:
111-
return jsonify({'error': 'Missing signing target (data, package_hash, or repodata_hash)'}), 400
107+
return jsonify({'error': 'Missing "data" field (base64 encoded content required)'}), 400
112108

113109
# Sign the data
114110
try:

server/signing_engine.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ def list_keys(self) -> List[Dict]:
4646
})
4747
return keys
4848

49-
def sign_data(self, data_hash: str, key_type: str, passphrase: str = None) -> str:
49+
def sign_data(self, data: bytes, key_type: str, passphrase: str = None) -> str:
5050
"""
5151
Sign data using specified key
5252
5353
Args:
54-
data_hash: Hash of data to sign (e.g., "sha256:abc123...")
54+
data: Raw data to sign (bytes)
5555
key_type: Type of key to use ('legacy' or 'modern')
5656
passphrase: GPG key passphrase
5757
@@ -62,9 +62,9 @@ def sign_data(self, data_hash: str, key_type: str, passphrase: str = None) -> st
6262

6363
logger.info(f"Signing data with {key_type} key ({key_id})")
6464

65-
# Sign the hash
65+
# Sign the data
6666
signed = self.gpg.sign(
67-
data_hash,
67+
data,
6868
keyid=key_id,
6969
passphrase=passphrase,
7070
detach=True,

tests/test_signing_fix.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
import sys
3+
import os
4+
import shutil
5+
import tempfile
6+
import unittest
7+
from unittest.mock import patch, MagicMock
8+
9+
# Add server to path
10+
sys.path.insert(0, '/usr/share/chelon/server')
11+
12+
from signing_engine import SigningEngine
13+
import gnupg
14+
15+
class TestSigningFix(unittest.TestCase):
16+
def setUp(self):
17+
self.temp_dir = tempfile.mkdtemp()
18+
self.gpg_home = os.path.join(self.temp_dir, '.gnupg')
19+
os.makedirs(self.gpg_home, mode=0o700)
20+
self.gpg = gnupg.GPG(gnupghome=self.gpg_home)
21+
22+
# Generate a key
23+
input_data = self.gpg.gen_key_input(
24+
key_type="RSA",
25+
key_length=1024,
26+
name_real="Test Key",
27+
name_email="test@example.com",
28+
no_protection=True
29+
)
30+
self.key = self.gpg.gen_key(input_data)
31+
self.key_id = self.key.fingerprint[-8:] # Get short ID
32+
33+
print(f"Generated test key: {self.key_id}")
34+
35+
def tearDown(self):
36+
shutil.rmtree(self.temp_dir)
37+
38+
def test_sign_binary_data(self):
39+
engine = SigningEngine(gnupg_home=self.gpg_home)
40+
41+
# Monkeypatch keys to use our test key
42+
with patch.dict(engine.KEYS, {'modern': self.key_id, 'legacy': '00000000'}, clear=True):
43+
data = b"Hello, World! This is binary data."
44+
45+
# Sign
46+
signature = engine.sign_data(data, 'modern')
47+
48+
print(f"Signature generated:\n{signature}")
49+
50+
# Write sig and data to temp files for robust verification
51+
sig_path = os.path.join(self.temp_dir, 'sig.asc')
52+
data_path = os.path.join(self.temp_dir, 'data.bin')
53+
54+
with open(sig_path, 'w') as f:
55+
f.write(signature)
56+
with open(data_path, 'wb') as f:
57+
f.write(data)
58+
59+
# Verify using file-based method
60+
with open(sig_path, 'rb') as f:
61+
verified = self.gpg.verify_file(f, data_path)
62+
63+
if not verified.valid:
64+
print(f"Verification failed!")
65+
print(f"Status: {verified.status}")
66+
print(f"Problems: {verified.problems}")
67+
print(f"Stderr:\n{verified.stderr}")
68+
self.assertTrue(verified.valid)
69+
self.assertEqual(verified.key_id, self.key.fingerprint[-16:]) # python-gnupg returns 16 char ID often
70+
71+
print("Verification successful!")
72+
73+
if __name__ == '__main__':
74+
unittest.main()

0 commit comments

Comments
 (0)