mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Test merge root certificates
This commit is contained in:
commit
b38e87b039
7 files changed
+470
-1
No files matched your search
@@ -12,6 +12,7 @@ python3 tools/riot-launcher-offline-check.py
|
||||
python3 tools/riot-memory-installer-offline-check.py
|
||||
python3 tools/riot-foundation-installer-offline-check.py
|
||||
python3 tools/riot-llvm-version-offline-check.py
|
||||
python3 -m unittest tests.test_root_certificates
|
||||
```
|
||||
|
||||
The Metal device-report check compiles on the host and needs no Darling:
|
||||
|
||||
@@ -56,6 +56,18 @@ The installer fetcher no longer treats a missing archive `Info.plist` or the inf
|
||||
|
||||
There is also a separate known compatibility gap: native LaunchAgent watch registration can return HTTP 409 after the League install path is accepted, leaving the download idle. A historical inspector hook handles this and interrupted `pending_verify` states in 28 mock cases, but that hook is disabled in normal launches and excluded from the manual workflow. There is no verified runtime-only replacement for that handoff in this snapshot. Do not assume the native permission and launcher fixes resolve every idle Install case, or enable debugger/API automation to work around it.
|
||||
|
||||
## `Unable to read system root certificates` or `Error -25263`
|
||||
|
||||
Riot logs `CertBundle: Unable to read system root certificates` and Chromium logs `[known_roots_mac.cc(60)] Unable to determine trusted roots; assuming all roots are trusted! Error -25263` when the prefix has no system trust domain. Security reads that domain from `/System/Library/Keychains/SystemRootCertificates.keychain` and `SystemTrustSettings.plist`, which Darling does not ship, so `SecTrustSettingsCopyCertificates` returns `errSecNoTrustSettings`.
|
||||
|
||||
`tools/ensure-darling-root-certificates.py` builds both from the host CA bundle (`/etc/ssl/cert.pem` or whichever bundle Python's `ssl` reports; `--bundle` overrides it). The launcher and `scripts/install-runtime.sh` run it. Each self-signed root gets an unrestricted trust entry. The keychain is built with the guest `security` tool in a single `darling shell`. A stamp in the same directory records the bundle's digest, so later launches skip it without starting Darling until the host bundle changes. If Darling was already running and the guest could not write the directory, the script installs the files on the host and says they apply after `darling shutdown`. A failure prints `Darling system root certificates were not installed: …` and the launch continues. To rebuild by hand:
|
||||
|
||||
```sh
|
||||
python3 build/runtime/tools/ensure-darling-root-certificates.py --force
|
||||
```
|
||||
|
||||
`python3 -m unittest tests.test_root_certificates` compares the generated SHA-1 keys, issuer names and serial numbers with OpenSSL, and checks that an unchanged bundle is skipped. It uses stub `darling` and `security` commands. Reading the keychain and the system trust domain inside a real guest (`security find-certificate -a /System/Library/Keychains/SystemRootCertificates.keychain`, `security dump-trust-settings -s`) is not yet confirmed.
|
||||
|
||||
## Riot closes after a network lookup is cancelled
|
||||
|
||||
Two local crashes were traced to `CFNetwork::_HostCancel` calling `CFRetain(NULL)`.
|
||||
|
||||
@@ -13,6 +13,9 @@ darling shell /usr/bin/true
|
||||
# installed layer and every write there (the Riot installer's temp files) fails with EACCES.
|
||||
mkdir -p "$HOME/.darling/private/tmp"
|
||||
chmod 1777 "$HOME/.darling/private/tmp"
|
||||
# Build the system root store now that the prefix exists; Darling is stopped next anyway.
|
||||
python3 build/runtime/tools/ensure-darling-root-certificates.py ||
|
||||
echo 'Darling system root store not installed; the launcher retries it' >&2
|
||||
python3 build/runtime/tools/stop-darling-runtime.py
|
||||
sh build/runtime/tools/install-riot-presentation-runtime.sh
|
||||
sh build/runtime/tools/install-riot-gpu-runtime.sh
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Check the system root store generator against OpenSSL without starting Darling.
|
||||
|
||||
A stub `darling` runs the guest script on the host with a stub `security` tool, so
|
||||
the generated trust record, the guest/host hand-off and the idempotency stamp are
|
||||
exercised; the real keychain database is not.
|
||||
"""
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import re
|
||||
import runpy
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / 'tools/ensure-darling-root-certificates.py'
|
||||
tool = runpy.run_path(str(SCRIPT))
|
||||
|
||||
DARLING_STUB = textwrap.dedent('''\
|
||||
#!/usr/bin/env python3
|
||||
import os, subprocess, sys
|
||||
with open(os.environ['STUB_LOG'], 'a') as log:
|
||||
log.write(' '.join(sys.argv[1:]) + '\\n')
|
||||
if os.environ.get('STUB_FAIL'):
|
||||
sys.exit(3)
|
||||
assert sys.argv[1] == 'shell'
|
||||
strip = lambda value: value[len('/Volumes/SystemRoot'):] if value.startswith('/Volumes/SystemRoot/') else value
|
||||
env = dict(os.environ, PATH=os.environ['STUB_BIN'] + ':' + os.environ['PATH'])
|
||||
sys.exit(subprocess.run([strip(arg) for arg in sys.argv[2:]], env=env).returncode)
|
||||
''')
|
||||
|
||||
SECURITY_STUB = textwrap.dedent('''\
|
||||
#!/usr/bin/env python3
|
||||
import hashlib, sys
|
||||
command, args = sys.argv[1], sys.argv[2:]
|
||||
if command == 'create-keychain':
|
||||
open(args[-1], 'wb').close()
|
||||
elif command == 'add-certificates':
|
||||
keychain = args[args.index('-k') + 1]
|
||||
with open(keychain, 'a') as stream:
|
||||
for path in args[args.index('-k') + 2:]:
|
||||
data = open(path, 'rb').read()
|
||||
stream.write(hashlib.sha1(data).hexdigest().upper() + '\\n')
|
||||
elif command == 'find-certificate':
|
||||
for line in open(args[-1]):
|
||||
print('keychain: "%s"' % args[-1])
|
||||
print('SHA-1 hash: ' + line.strip())
|
||||
''')
|
||||
|
||||
|
||||
def openssl(der, *options):
|
||||
return subprocess.run(['openssl', 'x509', '-inform', 'DER', '-noout', *options], input=der,
|
||||
capture_output=True, check=True).stdout.decode()
|
||||
|
||||
|
||||
def asn1_issuer(der):
|
||||
"""Issuer bytes located by OpenSSL's own ASN.1 parser: the fourth TBSCertificate field
|
||||
(third without an explicit version)."""
|
||||
lines = subprocess.run(['openssl', 'asn1parse', '-inform', 'DER'], input=der,
|
||||
capture_output=True, check=True).stdout.decode().splitlines()
|
||||
fields = []
|
||||
for line in lines:
|
||||
match = re.match(r'\s*(\d+):d=(\d+)\s+hl=(\d+)\s+l=\s*(\d+)\s+(\w+):\s*(.*)', line)
|
||||
if match and match.group(2) == '2':
|
||||
fields.append(match)
|
||||
if fields[0].group(6).startswith('cont [ 0 ]'):
|
||||
fields = fields[1:]
|
||||
offset, header, length = (int(fields[2].group(index)) for index in (1, 3, 4))
|
||||
return der[offset:offset + header + length]
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which('openssl'), 'openssl is required')
|
||||
class RootCertificateTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
self.bundle = tool['default_bundle']()
|
||||
self.data = self.bundle.read_bytes()
|
||||
self.prefix = self.root / 'prefix'
|
||||
self.prefix.mkdir()
|
||||
stubs = self.root / 'bin'
|
||||
stubs.mkdir()
|
||||
for name, text in (('darling', DARLING_STUB), ('security', SECURITY_STUB)):
|
||||
(stubs / name).write_text(text)
|
||||
(stubs / name).chmod(0o755)
|
||||
self.darling = [str(stubs / 'darling')]
|
||||
self.log = self.root / 'darling.log'
|
||||
self.log.touch()
|
||||
self.environment = {'STUB_LOG': str(self.log), 'STUB_BIN': str(stubs)}
|
||||
|
||||
def run_tool(self, bundle=None, **extra):
|
||||
output = io.StringIO()
|
||||
saved = dict(os.environ)
|
||||
os.environ.update(self.environment, **extra)
|
||||
try:
|
||||
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):
|
||||
status = tool['main'](['--prefix', str(self.prefix), '--bundle', str(bundle or self.bundle),
|
||||
'--darling', ' '.join(self.darling)])
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(saved)
|
||||
return status, output.getvalue()
|
||||
|
||||
def test_fields_match_openssl(self):
|
||||
roots = tool['load_roots'](self.data, report=lambda line: None)
|
||||
self.assertGreater(len(roots), 50)
|
||||
samples = [roots[0], roots[len(roots) // 2], roots[-1]]
|
||||
# Include a serial whose INTEGER needs a leading zero octet, if the bundle has one.
|
||||
samples += [cert for cert in roots if cert['serial'][0] == 0][:1]
|
||||
for cert in samples:
|
||||
fingerprint = openssl(cert['der'], '-fingerprint', '-sha1').split('=', 1)[1].strip()
|
||||
self.assertEqual(cert['sha1'], fingerprint.replace(':', ''))
|
||||
serial = openssl(cert['der'], '-serial').split('=', 1)[1].strip()
|
||||
self.assertEqual(int.from_bytes(cert['serial'], 'big'), int(serial, 16))
|
||||
self.assertEqual(cert['issuer'], asn1_issuer(cert['der']))
|
||||
|
||||
def test_trust_record_and_idempotency(self):
|
||||
status, output = self.run_tool()
|
||||
self.assertEqual(status, 0, output)
|
||||
folder = self.prefix / 'System/Library/Keychains'
|
||||
record = plistlib.loads((folder / 'SystemTrustSettings.plist').read_bytes())
|
||||
self.assertEqual(record['trustVersion'], 1)
|
||||
roots = tool['load_roots'](self.data, report=lambda line: None)
|
||||
self.assertEqual(set(record['trustList']), {cert['sha1'] for cert in roots})
|
||||
for cert in roots[:5]:
|
||||
entry = record['trustList'][cert['sha1']]
|
||||
self.assertEqual(entry['issuerName'], cert['issuer'])
|
||||
self.assertEqual(entry['serialNumber'], cert['serial'])
|
||||
self.assertEqual(entry['trustSettings'], [])
|
||||
self.assertEqual(hashlib.sha1(cert['der']).hexdigest().upper(), cert['sha1'])
|
||||
self.assertEqual(len((folder / 'SystemRootCertificates.keychain').read_text().split()), len(roots))
|
||||
self.assertEqual(len(self.log.read_text().splitlines()), 1)
|
||||
status, output = self.run_tool()
|
||||
self.assertEqual((status, output), (0, ''))
|
||||
self.assertEqual(len(self.log.read_text().splitlines()), 1, 'unchanged bundle restarted Darling')
|
||||
# A changed bundle is rebuilt.
|
||||
smaller = self.root / 'smaller.pem'
|
||||
smaller.write_bytes(b''.join(re.findall(rb'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----\n?',
|
||||
self.data, re.S)[:3]))
|
||||
status, output = self.run_tool(smaller)
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertEqual(len(self.log.read_text().splitlines()), 2)
|
||||
self.assertEqual(len(plistlib.loads((folder / 'SystemTrustSettings.plist').read_bytes())['trustList']), 3)
|
||||
|
||||
def test_failure_is_reported_and_not_stamped(self):
|
||||
status, output = self.run_tool(STUB_FAIL='1')
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn('Darling system root certificates were not installed', output)
|
||||
self.assertFalse((self.prefix / 'System/Library/Keychains' / tool['STAMP']).exists())
|
||||
kept = re.search(r'see (\S+)/guest\.log', output)
|
||||
self.assertTrue(kept, output)
|
||||
shutil.rmtree(kept.group(1))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Give Darling's system trust domain the host's CA roots.
|
||||
|
||||
Security.framework reads the system domain from two immutable files
|
||||
(libsecurity_keychain TrustSettingsSchema.h): the root store
|
||||
/System/Library/Keychains/SystemRootCertificates.keychain and its trust record
|
||||
SystemTrustSettings.plist. Darling ships neither, so SecTrustSettingsCopyCertificates
|
||||
for the system domain fails with errSecNoTrustSettings (-25263) and Chromium and
|
||||
Riot's CertBundle report that they cannot read the system roots.
|
||||
|
||||
The trust record is generated here from the host CA bundle. The keychain uses the
|
||||
guest `security` tool, since no host-side writer for its database format exists.
|
||||
Darling is only started when the bundle (or this script) changed; a stamp next to
|
||||
the outputs records the input digest.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import binascii
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import re
|
||||
import shutil
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
VERSION = 1
|
||||
KEYCHAIN = 'SystemRootCertificates.keychain'
|
||||
TRUST_SETTINGS = 'SystemTrustSettings.plist'
|
||||
STAMP = '.darling-lol-root-certificates.json'
|
||||
SYSTEM_ROOT = '/Volumes/SystemRoot'
|
||||
PEM = re.compile(rb'-----BEGIN CERTIFICATE-----\s*(.*?)\s*-----END CERTIFICATE-----', re.S)
|
||||
|
||||
# One guest invocation: build the keychain in the private work directory, list what
|
||||
# it holds, then publish both files from inside the guest so a running Darling sees
|
||||
# them (host writes to the prefix are invisible to a mounted overlay). The guest
|
||||
# cannot create directories in the root-owned base layer; the host then publishes.
|
||||
GUEST_SCRIPT = r'''#!/bin/sh
|
||||
set -u
|
||||
PATH=$PATH:/usr/bin:/bin
|
||||
work=$1
|
||||
kc=$work/SystemRootCertificates.keychain
|
||||
security create-keychain -p '' "$kc" || exit 10
|
||||
security unlock-keychain -p '' "$kc" || exit 11
|
||||
# Certificates the guest cannot parse are reported and left out; the host checks found.txt.
|
||||
security add-certificates -k "$kc" "$work"/certs/*.cer 2> "$work/add-errors.txt"
|
||||
security find-certificate -a -Z "$kc" > "$work/found.txt" || exit 12
|
||||
grep -q 'SHA-1 hash:' "$work/found.txt" || exit 13
|
||||
dest=/System/Library/Keychains
|
||||
if [ -d "$dest" ] && [ -w "$dest" ] &&
|
||||
cp "$kc" "$dest/.SystemRootCertificates.keychain.next" 2>/dev/null &&
|
||||
cp "$work/SystemTrustSettings.plist" "$dest/.SystemTrustSettings.plist.next" 2>/dev/null &&
|
||||
mv -f "$dest/.SystemRootCertificates.keychain.next" "$dest/SystemRootCertificates.keychain" &&
|
||||
mv -f "$dest/.SystemTrustSettings.plist.next" "$dest/SystemTrustSettings.plist"; then
|
||||
: > "$work/placed"
|
||||
else
|
||||
rm -f "$dest/.SystemRootCertificates.keychain.next" "$dest/.SystemTrustSettings.plist.next" 2>/dev/null
|
||||
fi
|
||||
exit 0
|
||||
'''
|
||||
|
||||
|
||||
class Failure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def default_bundle():
|
||||
paths = ssl.get_default_verify_paths()
|
||||
for candidate in (paths.cafile, paths.openssl_cafile, '/etc/ssl/certs/ca-certificates.crt',
|
||||
'/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/ca-bundle.pem', '/etc/ssl/cert.pem'):
|
||||
if candidate and Path(candidate).is_file():
|
||||
return Path(candidate)
|
||||
raise Failure('no host CA bundle found; pass --bundle')
|
||||
|
||||
|
||||
def der_element(data, offset):
|
||||
"""Return (tag, content start, end) of the DER element at offset."""
|
||||
if offset + 2 > len(data):
|
||||
raise ValueError('truncated element')
|
||||
tag = data[offset]
|
||||
if tag & 0x1f == 0x1f:
|
||||
raise ValueError('high tag numbers are not used in certificates')
|
||||
length = data[offset + 1]
|
||||
start = offset + 2
|
||||
if length & 0x80:
|
||||
count = length & 0x7f
|
||||
if count == 0 or count > 4 or start + count > len(data):
|
||||
raise ValueError('unsupported length')
|
||||
length = int.from_bytes(data[start:start + count], 'big')
|
||||
start += count
|
||||
end = start + length
|
||||
if end > len(data):
|
||||
raise ValueError('truncated element')
|
||||
return tag, start, end
|
||||
|
||||
|
||||
def parse_certificate(der):
|
||||
"""Fields TrustSettings::copyIssuerAndSerial stores: the issuer Name as encoded
|
||||
(CSSMOID_X509V1IssuerNameStd) and the serial INTEGER's content octets."""
|
||||
tag, start, end = der_element(der, 0)
|
||||
if tag != 0x30 or end != len(der):
|
||||
raise ValueError('not a single DER certificate')
|
||||
tag, offset, tbs_end = der_element(der, start)
|
||||
if tag != 0x30:
|
||||
raise ValueError('missing TBSCertificate')
|
||||
tag, content, end = der_element(der, offset)
|
||||
if tag == 0xa0: # explicit version
|
||||
offset = end
|
||||
tag, content, end = der_element(der, offset)
|
||||
if tag != 0x02 or content == end:
|
||||
raise ValueError('missing serial number')
|
||||
serial = der[content:end]
|
||||
offset = end
|
||||
for name in ('signature', 'issuer', 'validity', 'subject'):
|
||||
tag, content, end = der_element(der, offset)
|
||||
if tag != 0x30 or end > tbs_end:
|
||||
raise ValueError(f'missing {name}')
|
||||
if name == 'issuer':
|
||||
issuer = der[offset:end]
|
||||
elif name == 'subject':
|
||||
subject = der[offset:end]
|
||||
offset = end
|
||||
return {'sha1': hashlib.sha1(der).hexdigest().upper(), 'issuer': issuer,
|
||||
'serial': serial, 'subject': subject, 'der': der}
|
||||
|
||||
|
||||
def load_roots(data, report=print):
|
||||
roots, seen, identities, skipped = [], set(), {}, []
|
||||
for index, match in enumerate(PEM.finditer(data)):
|
||||
try:
|
||||
der = base64.b64decode(b''.join(match.group(1).split()), validate=True)
|
||||
cert = parse_certificate(der)
|
||||
except (binascii.Error, ValueError) as error:
|
||||
skipped.append(f'#{index + 1}: unreadable ({error})')
|
||||
continue
|
||||
if cert['sha1'] in seen:
|
||||
continue
|
||||
seen.add(cert['sha1'])
|
||||
if cert['subject'] != cert['issuer']:
|
||||
skipped.append(f'{cert["sha1"]}: not self-issued, not a root')
|
||||
continue
|
||||
# The keychain keys certificates by issuer and serial; a second one is a duplicate item.
|
||||
identity = (cert['issuer'], cert['serial'])
|
||||
if identity in identities:
|
||||
skipped.append(f'{cert["sha1"]}: same issuer and serial as {identities[identity]}')
|
||||
continue
|
||||
identities[identity] = cert['sha1']
|
||||
roots.append(cert)
|
||||
for line in skipped:
|
||||
report(f'Root certificates: skipped {line}')
|
||||
return roots
|
||||
|
||||
|
||||
def trust_settings(roots, now):
|
||||
# An empty trustSettings array means "trusted as root for every use"
|
||||
# (TrustSettings::evaluateCert / qualifyUsageWithCertDict, numSpecs == 0).
|
||||
return plistlib.dumps({
|
||||
'trustVersion': 1,
|
||||
'trustList': {cert['sha1']: {'issuerName': cert['issuer'], 'serialNumber': cert['serial'],
|
||||
'modDate': now, 'trustSettings': []} for cert in roots},
|
||||
}, fmt=plistlib.FMT_XML, sort_keys=True)
|
||||
|
||||
|
||||
def bundle_digest(data):
|
||||
return hashlib.sha256(b'darling-root-certificates-v%d\0' % VERSION + data).hexdigest()
|
||||
|
||||
|
||||
def current(folder, digest):
|
||||
try:
|
||||
stamp = json.loads((folder / STAMP).read_text())
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return (stamp.get('digest') == digest and (folder / KEYCHAIN).is_file()
|
||||
and (folder / TRUST_SETTINGS).is_file())
|
||||
|
||||
|
||||
def server_running(prefix):
|
||||
try:
|
||||
pid = int((prefix / '.init.pid').read_text().strip())
|
||||
status = (Path('/proc') / str(pid) / 'status').read_text()
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return not any(line.split()[1:2] == ['Z'] for line in status.splitlines() if line.startswith('State:'))
|
||||
|
||||
|
||||
def publish_host(work, folder):
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
for name in (KEYCHAIN, TRUST_SETTINGS):
|
||||
temporary = folder / f'.{name}.next'
|
||||
shutil.copyfile(work / name, temporary)
|
||||
os.chmod(temporary, 0o644)
|
||||
os.replace(temporary, folder / name)
|
||||
|
||||
|
||||
def write_stamp(folder, digest, bundle, roots, present):
|
||||
temporary = folder / (STAMP + '.next')
|
||||
temporary.write_text(json.dumps({'version': VERSION, 'digest': digest, 'bundle': str(bundle),
|
||||
'roots': len(roots), 'in_keychain': len(present)}, indent=1) + '\n')
|
||||
os.replace(temporary, folder / STAMP)
|
||||
|
||||
|
||||
def ensure(prefix, bundle, darling, force=False, timeout=600):
|
||||
data = bundle.read_bytes()
|
||||
digest = bundle_digest(data)
|
||||
folder = prefix / 'System/Library/Keychains'
|
||||
if not force and current(folder, digest):
|
||||
return 0
|
||||
roots = load_roots(data)
|
||||
if not roots:
|
||||
raise Failure(f'no root certificates in {bundle}')
|
||||
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0, tzinfo=None)
|
||||
work = Path(tempfile.mkdtemp(prefix='darling-root-certificates-')).resolve()
|
||||
keep = True
|
||||
try:
|
||||
(work / 'certs').mkdir()
|
||||
for index, cert in enumerate(roots):
|
||||
(work / 'certs' / f'{index:04d}.cer').write_bytes(cert['der'])
|
||||
(work / TRUST_SETTINGS).write_bytes(trust_settings(roots, now))
|
||||
script = work / 'build.sh'
|
||||
script.write_text(GUEST_SCRIPT)
|
||||
# Before Darling starts, the overlay picks up a host-created directory, which
|
||||
# lets the guest publish into it.
|
||||
running = server_running(prefix)
|
||||
if not running:
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
print(f'Building Darling system root store from {len(roots)} certificates in {bundle}', flush=True)
|
||||
try:
|
||||
with open(work / 'guest.log', 'wb') as log:
|
||||
result = subprocess.run([*darling, 'shell', '/bin/sh', SYSTEM_ROOT + str(script),
|
||||
SYSTEM_ROOT + str(work)], stdin=subprocess.DEVNULL,
|
||||
stdout=log, stderr=subprocess.STDOUT, timeout=timeout)
|
||||
except (OSError, subprocess.SubprocessError) as error:
|
||||
raise Failure(f'could not run the guest security tool: {error}; work files kept in {work}')
|
||||
if result.returncode or not (work / 'found.txt').is_file():
|
||||
raise Failure(f'guest keychain build failed (exit {result.returncode}); see {work}/guest.log')
|
||||
found = set(re.findall(r'SHA-1 hash:\s*([0-9A-Fa-f]{40})', (work / 'found.txt').read_text(errors='replace')))
|
||||
found = {value.upper() for value in found}
|
||||
wanted = {cert['sha1'] for cert in roots}
|
||||
present = wanted & found
|
||||
if not present:
|
||||
raise Failure(f'the guest keychain holds none of the certificates; see {work}')
|
||||
if present != wanted:
|
||||
print(f'Root certificates: the guest rejected {len(wanted - present)} of {len(wanted)}; '
|
||||
f'see {work}/add-errors.txt', file=sys.stderr, flush=True)
|
||||
# Kept for inspection; the outputs are still published.
|
||||
else:
|
||||
keep = False
|
||||
if not (work / 'placed').exists():
|
||||
# A running guest could not see a new directory; publish on the host for
|
||||
# the next Darling start.
|
||||
publish_host(work, folder)
|
||||
if running:
|
||||
print('Darling system root store installed; it takes effect after `darling shutdown`',
|
||||
file=sys.stderr, flush=True)
|
||||
write_stamp(folder, digest, bundle, roots, present)
|
||||
print(f'Darling system root store has {len(present)} certificates', flush=True)
|
||||
return 0
|
||||
except OSError as error:
|
||||
raise Failure(f'{error}; work files kept in {work}')
|
||||
finally:
|
||||
if not keep:
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__.split('\n\n')[0])
|
||||
parser.add_argument('--bundle', type=Path, help='PEM CA bundle (default: the host system bundle)')
|
||||
parser.add_argument('--prefix', type=Path, default=Path(os.environ.get('DPREFIX', str(Path.home() / '.darling'))))
|
||||
parser.add_argument('--darling', default='darling', help='Darling command (for tests)')
|
||||
parser.add_argument('--force', action='store_true', help='rebuild even when the stamp is current')
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
return ensure(args.prefix.expanduser(), args.bundle or default_bundle(), args.darling.split(), args.force)
|
||||
except (Failure, OSError) as error:
|
||||
print(f'Darling system root certificates were not installed: {error}', file=sys.stderr, flush=True)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -60,7 +60,8 @@ with open({str(log)!r}, "a") as output:
|
||||
records = [json.loads(line) for line in log.read_text().splitlines()]
|
||||
maintenance = [r for r in records if r["command"] == "python3"]
|
||||
assert [Path(r["args"][0]).name for r in maintenance] == [
|
||||
"ensure-darling-runtime.py", "ensure-darling-service-accounts.py", "ensure-league-runtime-layout.py"
|
||||
"ensure-darling-runtime.py", "ensure-darling-service-accounts.py", "ensure-league-runtime-layout.py",
|
||||
"ensure-darling-root-certificates.py",
|
||||
], "unexpected automatic helper"
|
||||
launches = [r for r in records if r["command"] == "darling"]
|
||||
assert len(launches) == 1, "unexpected launch count"
|
||||
|
||||
@@ -11,6 +11,11 @@ runner_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
python3 "$runner_dir/ensure-darling-runtime.py"
|
||||
python3 "$runner_dir/ensure-darling-service-accounts.py"
|
||||
python3 "$runner_dir/ensure-league-runtime-layout.py"
|
||||
# Security's system trust domain (SecTrustSettingsCopyCertificates) needs a root keychain and
|
||||
# trust record built from the host CA bundle. Only a changed bundle starts Darling; a failure
|
||||
# is reported but does not stop the launch.
|
||||
python3 "$runner_dir/ensure-darling-root-certificates.py" ||
|
||||
echo 'Continuing without a Darling system root store; TLS clients may report untrusted roots' >&2
|
||||
# Discard argument-rewriting settings from the former injected shim.
|
||||
unset RCLOL_APP_ARGS RCLOL_APP_ARGS_SLOT RCLOL_APP_ARG2 RCLOL_APP_ARG2_SLOT
|
||||
unset RCLOL_APP_ARG3 RCLOL_APP_ARG3_SLOT RCLOL_UX_ARG1 RCLOL_UX_ARG1_SLOT
|
||||
|
||||
Reference in new issue
Block a user