Files
archy/tests/regression/c01-running-services.py
T

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