200 lines
9.9 KiB
Python
200 lines
9.9 KiB
Python
#!/usr/bin/python3
|
|
"""Genuine FIPS/WG/Tor/NM startup in the disposable PID1 network realm.
|
|
|
|
Uses captured public executables/dependencies and new synthetic keys only.
|
|
Never reads installed-node private keys or changes physical host networking.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import pwd
|
|
import secrets
|
|
import socket
|
|
import struct
|
|
import time
|
|
|
|
|
|
def install(c):
|
|
assert os.geteuid()==0 and Path('/run/.containerenv').exists()
|
|
assert os.uname().nodename=='c01-prerequisite-qualification' and os.environ.get('C01_RUNNING_SERVICES')=='1'
|
|
run,write,dump,sha=c['run'],c['write'],c['dump'],c['sha']
|
|
package,evidence,policy=c['PACKAGE'],c['EVIDENCE'],c['policy']
|
|
import c01_admission as admission
|
|
import c01_network_owner as network
|
|
stack=json.loads((package/'service-stack-manifest.json').read_text())['files']
|
|
for executable in ('usr/bin/fips','usr/bin/tor','usr/bin/wg','usr/sbin/NetworkManager'):
|
|
assert sha('/'+executable)==stack[executable]['sha256'], 'actual installed public program bytes required'
|
|
launch,_=policy.json_read(policy.LAUNCH)
|
|
run('systemctl','stop','NetworkManager.service','tor@default.service','tor.service')
|
|
identity=Path(launch['runtime_data_dir'] if os.environ.get('C01_LAYOUT_MIGRATION')=='1' else launch['manager_backing'])/'identity'
|
|
secret=secrets.token_bytes(32)
|
|
der=bytes.fromhex('302e0201010420')+secret+bytes.fromhex('a00706052b8104000a')
|
|
public=run('openssl','ec','-inform','DER','-pubout','-conv_form','compressed','-outform','DER',input=der).stdout[-33:][1:]
|
|
assert len(public)==32 and list(public)!=launch['binding']['local_identity']
|
|
for name,data,mode in (('fips_key',secret.hex(),0o600),('fips_key.pub',public,0o644)):
|
|
assert not (identity/name).exists();write(identity/name,data,mode);os.chown(identity/name,1000,1000)
|
|
write('/etc/fips/fips.key',secret.hex(),0o600);write('/etc/fips/fips.pub',public,0o644)
|
|
write('/etc/fips/fips.yaml','''node:
|
|
identity:
|
|
persistent: true
|
|
discovery:
|
|
lan:
|
|
enabled: false
|
|
tun:
|
|
enabled: true
|
|
name: fips0
|
|
mtu: 1280
|
|
dns:
|
|
enabled: true
|
|
bind_addr: 127.0.0.1
|
|
transports:
|
|
udp:
|
|
bind_addr: 127.0.0.1:8668
|
|
tcp:
|
|
bind_addr: 127.0.0.1:8668
|
|
peers: []
|
|
''',0o644)
|
|
c['unit']('archipelago-fips.service',(package/'image-recipe/configs/archipelago-fips.service').read_text())
|
|
write('/etc/fips/fips.nft','''table inet fips {
|
|
chain input {
|
|
type filter hook input priority 0; policy accept;
|
|
iifname "fips0" jump fips_in;
|
|
}
|
|
chain fips_in {
|
|
ct state established,related accept;
|
|
include "/etc/fips/fips.d/*.nft"
|
|
drop;
|
|
}
|
|
}
|
|
''',0o644)
|
|
for filename in ('80-web-ui.nft','85-app-ports.nft','90-ssh.nft'):
|
|
write('/etc/fips/fips.d/'+filename,'# Synthetic preserved FIPS policy: no public inbound ports.\n',0o644)
|
|
run('/usr/sbin/nft','--check','--file','/etc/fips/fips.nft')
|
|
run('/usr/sbin/nft','--file','/etc/fips/fips.nft')
|
|
private=run('wg','genkey').stdout
|
|
Path('/etc/wireguard').mkdir(mode=0o700,exist_ok=True)
|
|
write('/etc/wireguard/c01-fixture.key',private,0o600)
|
|
write('/etc/archipelago-fips-wg/wg.public',run('wg','pubkey',input=private).stdout,0o644)
|
|
c['unit']('archipelago-wg.service','''[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStart=/usr/bin/ip link add wg0 type wireguard
|
|
ExecStart=/usr/bin/wg set wg0 private-key /etc/wireguard/c01-fixture.key
|
|
ExecStart=/usr/bin/ip address add 10.44.0.1/24 dev wg0
|
|
ExecStart=/usr/bin/ip link set wg0 up
|
|
ExecStop=/usr/bin/ip link delete wg0
|
|
NoNewPrivileges=yes
|
|
CapabilityBoundingSet=CAP_NET_ADMIN
|
|
''')
|
|
tor_account=pwd.getpwnam('debian-tor')
|
|
assert tor_account.pw_uid not in (0,1000,1001,61001,61002)
|
|
os.chown('/var/lib/tor',tor_account.pw_uid,tor_account.pw_gid)
|
|
hidden=Path('/var/lib/tor/hidden_service_archipelago')
|
|
hidden.mkdir(mode=0o700,exist_ok=True)
|
|
assert not list(hidden.iterdir()),'fixture must not replace hidden-service keys'
|
|
os.chown(hidden,tor_account.pw_uid,tor_account.pw_gid);hidden.chmod(0o700)
|
|
Path('/run/tor').mkdir(mode=0o700,exist_ok=True);os.chown('/run/tor',tor_account.pw_uid,tor_account.pw_gid)
|
|
write('/etc/tor/torrc','''User debian-tor
|
|
DataDirectory /var/lib/tor
|
|
SocksPort 127.0.0.1:9050
|
|
ControlSocket /run/tor/c01-fixture-control
|
|
CookieAuthentication 1
|
|
CookieAuthFile /run/tor/c01-fixture-cookie
|
|
RunAsDaemon 0
|
|
HiddenServiceDir /var/lib/tor/hidden_service_archipelago
|
|
HiddenServicePort 80 127.0.0.2:80
|
|
''',0o644)
|
|
commands={'fips':['/usr/bin/fips','--config','/etc/fips/fips.yaml'],
|
|
'tor':['/usr/bin/tor','-f','/etc/tor/torrc','--RunAsDaemon','0'],
|
|
'network_mutations':['/usr/sbin/NetworkManager','--no-daemon']}
|
|
write('/usr/lib/systemd/system/tor@default.service','''[Unit]
|
|
After=network.target
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/tor -f /etc/tor/torrc --RunAsDaemon 0
|
|
Restart=no
|
|
NoNewPrivileges=yes
|
|
''',0o644)
|
|
write('/etc/NetworkManager/NetworkManager.conf','''[main]
|
|
plugins=ifupdown,keyfile
|
|
no-auto-default=*
|
|
[ifupdown]
|
|
managed=false
|
|
[device-fixture]
|
|
match-device=interface-name:*
|
|
managed=0
|
|
''',0o600)
|
|
write('/usr/lib/systemd/system/NetworkManager.service','''[Unit]
|
|
After=dbus.service
|
|
Requires=dbus.service
|
|
[Service]
|
|
Type=dbus
|
|
BusName=org.freedesktop.NetworkManager
|
|
ExecStart=/usr/sbin/NetworkManager --no-daemon
|
|
Restart=no
|
|
NoNewPrivileges=yes
|
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
|
''',0o644)
|
|
run('systemctl','daemon-reload')
|
|
run('systemctl','start','archipelago-fips.service','archipelago-wg.service','tor@default.service','NetworkManager.service')
|
|
end=time.monotonic()+180;last='not started'
|
|
while time.monotonic()<end:
|
|
try:
|
|
cookie=Path('/run/tor/c01-fixture-cookie').read_bytes();assert len(cookie)==32
|
|
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as channel:
|
|
channel.settimeout(2);channel.connect('/run/tor/c01-fixture-control')
|
|
channel.sendall(('AUTHENTICATE '+cookie.hex()+'\r\n').encode())
|
|
assert channel.recv(4096).startswith(b'250 OK')
|
|
channel.sendall(b'GETINFO status/bootstrap-phase\r\n')
|
|
last=channel.recv(16384).decode()
|
|
if 'PROGRESS=100' in last:break
|
|
except (OSError,AssertionError):pass
|
|
time.sleep(1)
|
|
else:raise AssertionError('genuine Tor bootstrap unavailable: '+last)
|
|
with socket.create_connection(('127.0.0.1',9050),timeout=3) as connection:
|
|
connection.sendall(b'\x05\x01\x00');assert connection.recv(2)==b'\x05\x00'
|
|
# A real DNS reply is required before capturing the immutable FIPS assets.
|
|
query=struct.pack('!6H',19283,0x100,1,0,0,0)+b'\x0bc01-fixture\x07invalid\x00'+struct.pack('!HH',1,1)
|
|
with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as dns:
|
|
dns.settimeout(5);dns.sendto(query,('127.0.0.1',5354))
|
|
answer,_=dns.recvfrom(4096);assert len(answer)>=12 and answer[:2]==query[:2] and answer[2]&0x80
|
|
# Keep the production runtime guard. Reappoint the network owner before any
|
|
# access/application lease exists; existing producer identities are measured.
|
|
run('systemctl','stop','archipelago-c01-network.service')
|
|
network_value=json.loads(network.CONFIG.read_text())
|
|
for name,unit,protocol,port in (('fips-dns','archipelago-fips.service','udp',5354),('tor-socks','tor@default.service','tcp',9050)):
|
|
info=dict(line.split('=',1) for line in run('systemctl','show',unit,'-p','MainPID','-p','ControlGroup').stdout.decode().splitlines())
|
|
pid=int(info['MainPID']);assert pid>1
|
|
uid=int(next(line for line in Path(f'/proc/{pid}/status').read_text().splitlines() if line.startswith('Uid:')).split()[1])
|
|
network_value['routes'].append(dict(name=name,source='managed',target='host',protocol=protocol,port=port,
|
|
app_id=None,projection_sha256=None,target_cgroup=info['ControlGroup'],target_uid=uid))
|
|
dump(network.CONFIG,network_value)
|
|
run('systemctl','start','archipelago-c01-network.service')
|
|
plan=json.loads(Path('/etc/archipelago-fips-wg/plan.json').read_text())
|
|
plan.update(fips='running',wg='running',assets={name:sha(path) for name,path in admission.FIPS_ASSETS.items()},
|
|
programs={name:sha(admission.ASSETS[name]) for name in ('wg','systemctl')})
|
|
dump('/etc/archipelago-fips-wg/plan.json',plan)
|
|
assets=dict(admission.ASSETS)
|
|
for mapping in admission.SERVICE_ASSETS.values():assets.update(mapping)
|
|
contract={'version':2,'platform_sha256':launch['platform_profile_sha256'],'roles':sorted(admission.ROLES),
|
|
'assets':{name:sha(path) for name,path in assets.items()},'services':{name:'running' for name in admission.UNITS},
|
|
'optional_unavailable':[],'required_routes':['management','fips-dns','tor-socks'],'fips_public_key':public.hex(),
|
|
'service_argv_sha256':{name:hashlib.sha256(b'\0'.join(x.encode() for x in argv)+b'\0').hexdigest() for name,argv in commands.items()}}
|
|
contract['service_argv_sha256']['wg']=None
|
|
admission.validate(contract,launch)
|
|
raw=(json.dumps(contract,sort_keys=True,indent=2)+'\n').encode();digest=hashlib.sha256(raw).hexdigest()
|
|
write(admission.POLICIES/(digest+'.json'),raw,0o600)
|
|
launch['admission_sha256']=digest;dump(policy.LAUNCH,launch)
|
|
held=admission.Contract(launch)
|
|
try:held.check();states=held.initial
|
|
finally:held.close()
|
|
result={'scope':'genuine running-service startup; synthetic keys and private network only',
|
|
'policy_sha256':digest,'services':states,'tor_bootstrap':last,'fips_public':public.hex(),
|
|
'node_public':bytes(launch['binding']['local_identity']).hex(),'programs':{p:sha('/'+p) for p in
|
|
('usr/bin/fips','usr/bin/tor','usr/bin/wg','usr/sbin/NetworkManager')}}
|
|
dump(evidence/'running-services.json',result)
|
|
dump(evidence/'conditional-admission.json',{'policy_sha256':digest,'optional_unavailable':[],
|
|
'scope':'running-service startup; data/migration and operational mutation qualification recorded separately'})
|
|
return result
|