Skip to content

Commit cbc3e7d

Browse files
committed
wip: add secrets sync script and self-check workflow (hash-only)
1 parent b226a9e commit cbc3e7d

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Secrets Self-Check
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
check:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Print SHA256 of Central creds and GPG secrets
11+
env:
12+
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
13+
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
14+
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
15+
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
16+
GPG_KEYNAME: ${{ secrets.GPG_KEYNAME }}
17+
run: |
18+
set -euo pipefail
19+
creds_hash=$(printf "%s:%s" "$CENTRAL_USERNAME" "$CENTRAL_PASSWORD" | sha256sum | awk '{print $1}')
20+
pass_hash=$(printf "%s" "$GPG_PASSPHRASE" | sha256sum | awk '{print $1}')
21+
keyname_hash=$(printf "%s" "$GPG_KEYNAME" | sha256sum | awk '{print $1}')
22+
# Normalize multi-line private key before hashing
23+
priv_hash=$(printf "%s" "$GPG_PRIVATE_KEY" | sha256sum | awk '{print $1}')
24+
echo "SHA256 central(user:pass): $creds_hash"
25+
echo "SHA256 gpg.passphrase: $pass_hash"
26+
echo "SHA256 gpg.keyname(fingerprint): $keyname_hash"
27+
echo "SHA256 armored private key: $priv_hash"
28+

scripts/sync-central-secrets.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
import argparse, os, sys, subprocess, xml.etree.ElementTree as ET, hashlib, base64, shlex
3+
4+
def die(msg):
5+
sys.stderr.write(msg+"\n"); sys.exit(1)
6+
7+
def which(cmd):
8+
from shutil import which as w
9+
return w(cmd)
10+
11+
def run(cmd, input_bytes=None):
12+
p = subprocess.Popen(cmd, stdin=subprocess.PIPE if input_bytes else None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
13+
out, err = p.communicate(input=input_bytes)
14+
if p.returncode != 0:
15+
raise RuntimeError("command failed: %s\n%s" % (" ".join(cmd), err.decode('utf-8','replace')))
16+
return out
17+
18+
def sha256_str(s: str) -> str:
19+
return hashlib.sha256(s.encode('utf-8')).hexdigest()
20+
21+
def main():
22+
ap = argparse.ArgumentParser(description='Sync Central/GPG secrets from local ~/.m2/settings.xml and gpg to GitHub Actions secrets (hashed preview + optional set).')
23+
ap.add_argument('--settings', default=os.path.expanduser('~/.m2/settings.xml'))
24+
ap.add_argument('--repo', default=None, help='owner/repo for gh secret operations (defaults to current)')
25+
ap.add_argument('--key-id', default=None, help='GPG key id/fingerprint to export; auto-detect if omitted')
26+
ap.add_argument('--set', action='store_true', help='Write secrets to GitHub (CENTRAL_USERNAME, CENTRAL_PASSWORD, GPG_PASSPHRASE, GPG_PRIVATE_KEY, GPG_KEYNAME)')
27+
ap.add_argument('--delete-first', action='store_true', help='Delete existing secrets before setting')
28+
args = ap.parse_args()
29+
30+
if not os.path.exists(args.settings):
31+
die('Settings not found: %s' % args.settings)
32+
33+
# Parse settings.xml
34+
tree = ET.parse(args.settings)
35+
root = tree.getroot()
36+
37+
ns = {}
38+
# servers/server[id=central]
39+
central_user = central_pass = None
40+
for srv in root.findall('./servers/server', ns):
41+
sid = (srv.findtext('id') or '').strip()
42+
if sid == 'central':
43+
central_user = (srv.findtext('username') or '').strip()
44+
central_pass = (srv.findtext('password') or '').strip()
45+
break
46+
if not central_user or not central_pass:
47+
die('Could not find <server id="central"> with username/password in %s' % args.settings)
48+
49+
# gpg.passphrase (unique)
50+
passphrases = []
51+
for prop in root.findall('.//profiles/profile/properties', ns):
52+
val = prop.findtext('gpg.passphrase')
53+
if val and val.strip():
54+
passphrases.append(val.strip())
55+
uniq = sorted(set(passphrases))
56+
if len(uniq) != 1:
57+
die('Expected exactly one gpg.passphrase; found %d: %s' % (len(uniq), ','.join(uniq)))
58+
gpg_pass = uniq[0]
59+
60+
if not which('gpg'):
61+
die('gpg not found on PATH')
62+
63+
key_id = args.key_id
64+
if not key_id:
65+
# auto-detect signing-capable key (sec with s capability)
66+
out = run(['gpg','--list-secret-keys','--with-colons']).decode('utf-8','replace').splitlines()
67+
for line in out:
68+
parts = line.split(':')
69+
if len(parts) > 12 and parts[0] == 'sec' and 's' in parts[11]:
70+
key_id = parts[4]
71+
break
72+
if not key_id:
73+
die('No signing-capable secret key found (looked for sec with s capability)')
74+
75+
# fingerprint from key
76+
fpr = None
77+
out = run(['gpg','--with-colons','--list-secret-keys', key_id]).decode('utf-8','replace').splitlines()
78+
for line in out:
79+
parts = line.split(':')
80+
if parts and parts[0] == 'fpr' and len(parts) > 9:
81+
fpr = parts[9]
82+
break
83+
if not fpr:
84+
die('Could not extract fingerprint for key %s' % key_id)
85+
86+
# export armored private key using loopback/passphrase; if agent rejects, fallback without pass
87+
try:
88+
armored = run(['gpg','--batch','--yes','--pinentry-mode','loopback','--passphrase', gpg_pass, '--armor','--export-secret-keys', key_id]).decode('utf-8','replace')
89+
except Exception:
90+
armored = run(['gpg','--armor','--export-secret-keys', key_id]).decode('utf-8','replace')
91+
92+
# Hashes for comparison
93+
creds_hash = sha256_str(central_user + ':' + central_pass)
94+
pass_hash = sha256_str(gpg_pass)
95+
fpr_hash = sha256_str(fpr)
96+
priv_hash = hashlib.sha256(armored.encode('utf-8')).hexdigest()
97+
98+
print('Central user: %s' % central_user)
99+
print('Key fingerprint: %s' % fpr)
100+
print('SHA256 central(user:pass): %s' % creds_hash)
101+
print('SHA256 gpg.passphrase: %s' % pass_hash)
102+
print('SHA256 gpg.keyname(fingerprint): %s' % fpr_hash)
103+
print('SHA256 armored private key: %s' % priv_hash)
104+
105+
if args.set:
106+
if not which('gh'):
107+
die('gh not found on PATH (needed for --set)')
108+
repo_flag = ['--repo', args.repo] if args.repo else []
109+
if args.delete_first:
110+
for name in ['CENTRAL_USERNAME','CENTRAL_PASSWORD','GPG_PASSPHRASE','GPG_PRIVATE_KEY','GPG_KEYNAME']:
111+
subprocess.run(['gh','secret','delete',name,'--app','actions'] + repo_flag, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
112+
# set secrets
113+
for name, value in [
114+
('CENTRAL_USERNAME', central_user),
115+
('CENTRAL_PASSWORD', central_pass),
116+
('GPG_PASSPHRASE', gpg_pass),
117+
('GPG_PRIVATE_KEY', armored),
118+
('GPG_KEYNAME', fpr),
119+
]:
120+
proc = subprocess.Popen(['gh','secret','set',name,'--app','actions'] + repo_flag, stdin=subprocess.PIPE)
121+
proc.communicate(input=value.encode('utf-8'))
122+
if proc.returncode != 0:
123+
die('failed to set secret %s' % name)
124+
print('Secrets updated in GitHub (Actions app).')
125+
126+
if __name__ == '__main__':
127+
main()
128+

0 commit comments

Comments
 (0)