railiance-cluster/tools/public-tls-metadata.py
codex c562d7134c
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
feat(recovery): prepare owner-bound offline TLS continuity preflight
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
2026-10-17 02:12:35 +02:00

55 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Owner-run public certificate/trust metadata; no private-key argument or network.
The owner supplies public-only PEM files from the independently established
original recovery source. This does not identify that source or accept reissue.
"""
import argparse
from datetime import timezone
import hashlib
import json
import os
from pathlib import Path
import re
import stat
from cryptography import x509
from cryptography.hazmat.primitives import serialization
LIMIT=1024*1024
CERT=re.compile(br'-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+-----END CERTIFICATE-----')
def public_bundle(path):
# Bound the read before allocation; refuse every non-certificate PEM block.
path=Path(os.path.abspath(path))
if any(p.is_symlink() for p in (path,*path.parents)) or not stat.S_ISREG(path.stat().st_mode) or not 0<path.stat().st_size<=LIMIT:raise ValueError()
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
with os.fdopen(fd,'rb') as source:
if not stat.S_ISREG(os.fstat(source.fileno()).st_mode) or not 0<os.fstat(source.fileno()).st_size<=LIMIT:raise ValueError()
raw=source.read(LIMIT+1)
if len(raw)>LIMIT:raise ValueError()
blocks=CERT.findall(raw)
if not blocks or len(blocks)>512 or CERT.sub(b'',raw).strip():raise ValueError()
return raw,[x509.load_pem_x509_certificate(block) for block in blocks]
def describe(cert_path,trust_path):
certificate,chain=public_bundle(cert_path);trust,roots=public_bundle(trust_path);leaf=chain[0]
try:names=leaf.extensions.get_extension_for_class(x509.SubjectAlternativeName).value.get_values_for_type(x509.DNSName)
except x509.ExtensionNotFound:names=[]
spki=leaf.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo)
def utc_date(name):
value=getattr(leaf,name+'_utc',None)
if value is None:value=getattr(leaf,name).replace(tzinfo=timezone.utc)
return value.isoformat()
return {'schema':'nk49.public-tls-metadata.v1','status':'public_metadata_only_not_originality_or_owner_assent',
'tls_cert_sha256':hashlib.sha256(certificate).hexdigest(),'trust_sha256':hashlib.sha256(trust).hexdigest(),
'leaf_public_spki_sha256':'sha256:'+hashlib.sha256(spki).hexdigest(),'leaf_dns_sans':names,
'leaf_not_before':utc_date('not_valid_before'),'leaf_not_after':utc_date('not_valid_after'),
'server_chain_certificates':len(chain),'trust_certificates':len(roots),'network_contacted':False,'private_key_argument_supported':False}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--public-cert',required=True);p.add_argument('--public-trust',required=True);a=p.parse_args()
try:result=describe(a.public_cert,a.public_trust)
except Exception:print('{"status":"public_metadata_refused","private_diagnostics_suppressed":true}');return 1
print(json.dumps(result,indent=2));return 0
if __name__=='__main__':raise SystemExit(main())