Skip to content

About

Static Rust first-contact helper for Liskov-managed Acurast Cargo/PRoot workloads

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

liskov-runtime-cargo

liskov-runtime-contact is the native bootstrap and process supervisor for Liskov-managed workloads running in Acurast Cargo/PRoot images.

It discovers the active Acurast deployment and processor through the Cargo bridge, signs one Liskov runtime-bootstrap v2 request with the deployment's Ed25519 key and waits for bounded authenticated contact. When that bound response enables runtime environment retrieval, the helper signs a separate UID-bound v2 request, rejects endpoint or response substitution, validates every environment name, and captures those values before customer startup. It then becomes a child subreaper, starts the customer command in a dedicated process group, forwards supported signals, reaps descendants, and returns the customer's exact exit status or terminating signal after cleanup. If identity, signing, transport, or bootstrap validation fails, the customer command does not start.

When the signed bootstrap envelope contains the Liskov-owned x.pc extension, the helper also emits bounded pre-contact evidence before bridge discovery and once more on terminal contact failure. The envelope is read from LISKOV_BOOTSTRAP only; the retired PROOF_SLIPWAY_BOOTSTRAP spelling is no longer read. This bearer-authenticated evidence is diagnostic only: it never authorizes command execution or replaces the signed runtime-bootstrap gate.

Supported runtime

  • 64-bit AArch64 Cargo/PRoot workloads
  • Acurast Android processor runtime 1.25.0 or newer
  • an HTTPS Liskov core endpoint

The release binary is statically linked with Rustls and bundled Web PKI roots. It does not depend on OpenSSL or the image's CA store.

Usage

liskov-runtime-contact [--core-url URL] -- <command> [args...]

For example:

./liskov-runtime-contact -- /opt/my-app/bin/server --listen 0.0.0.0:8080

The core URL is selected in this order:

  1. --core-url URL
  2. LISKOV_CORE_URL
  3. https://runtime.liskov.proof.computer

runtime.liskov.proof.computer is the fleet's own control-plane name. It is deliberately not the operator console hostname, so console protection rules, maintenance, and edge changes cannot wedge running jobs.

Only HTTPS URLs without user information, a query, or a fragment are accepted. BRIDGE_SOCKET is required and is supplied by the Acurast Cargo runtime.

The supervisor writes concise, non-secret diagnostics to stderr. It never logs bridge replies, signed request or response bodies, signatures, processor identity, pre-contact tokens, or the customer command.

After successful contact it also emits best-effort, deployment-signed Cargo diagnostic-v4 observations. These observations use one monotonic sequence for the runtime instance and include nested process-attempt counters. Delivery is bounded to five seconds per request with a bounded response and never changes workload execution.

Release 0.10.20 also ships a dormant, bounded cargo-baseline-v1 processor- fact collector. It performs no reads, signing, DNS, or additional HTTP when the authenticated bootstrap omits processorFacts, as current servers do. A valid short-lived authorization is removed from the retained bootstrap before every other consumer, must pin the running helper version and /proc/self/exe digest, and can request only the due Android-corroboration, execution-surface, or controlled-egress dimensions. Collection runs once in a silent detached worker immediately before the first customer-process spawn and cannot affect startup, supervision, signals, restarts, or exit status. The exact closed wire, privacy, file, network, signature, and delivery contract is documented in docs/processor-fact-contract.md.

Optional stdout/stderr forwarding

Output forwarding starts only when the signed bootstrap response contains the exact policy decision {"logging":{"enabled":true}} and BLACKBOX_LOG_CONFIG validates against the same application, deployment, and job. A signed runtime-environment value takes precedence over an inherited process value. When neither contains the config, the supervisor uses the signed /api/jobs/secret-bootstrap protocol to discover the exact job grant, then requests only blackbox-log-config and decrypts its response through the Acurast bridge. The transitional job-bound Lockbox bootstrap value is still accepted when present, read only from LISKOV_LOCKBOX_BOOTSTRAP. Both paths verify the response, encrypted-payload, AAD, plaintext, and secret bindings before installing the value in memory. This narrow lookup lets the controller start before Runtime SSH without making the supervisor the owner of customer secrets. Lookup failure is non-fatal to the workload and access attachment.

The supervisor tees stdout and stderr to their normal local destinations, then forwards separately encrypted records through Liskov's canonical Blackbox sink protocol. UTF-8 chunks are labeled as text; binary chunks use base64url framing. Every encrypted record binds the stream, process attempt, monotonic output sequence, runtime instance, timestamp, byte length, and truncation state.

Capture uses 3-KiB chunks, a 256-KiB-per-second admission limit, a 128-item in-memory queue, at most 32 records and 256 KiB per request, five-second requests, 16-KiB responses, and at most ten network requests per second. It does not create a local durable spool. Queue or rate overflow produces only bounded encrypted dropped-byte and dropped-chunk evidence. Network failure, sink backpressure, invalid logging configuration, and the bounded 250-ms terminal flush never block, restart, terminate, or change the customer result. Customer output, the Blackbox DEK, factory token, request signatures, and environment values never enter Cargo diagnostics.

The server-owned loggingOutageCanary bootstrap flag is accepted only when logging is already explicitly enabled and the bound Blackbox configuration is valid. For an exact release canary it makes every log request fail locally, without changing capture limits, local stdout/stderr, supervision, diagnostics, or the customer result. It is not a customer-authored policy field and never enables logging by itself.

When Runtime SSH is attached and logging is enabled, the server may additionally authorize a closed diagnostics.providerLogs mode. sanitized emits typed setup, ready, degraded, crash, and stopped lifecycle records. The temporary raw_tailscaled_stderr mode adds only tailscaled stderr for an exact server-selected canary until its signed expiry. Every raw line is converted to valid UTF-8, capped at 8 KiB, and stripped of the in-memory auth key and recognized credential forms before encryption. Runtime SSH has its own 64-KiB/s, 128-line, 1-MiB queue, but shares one ordered Blackbox writer with customer output; customer records are always serviced first and origins never share an encrypted batch. Provider logging failure cannot affect workload startup, supervision, health, or the customer exit result.

Exit status

Status Meaning
2 CLI or configuration error
70 Bridge, identity, signature, protocol, or permanent server rejection
75 Transport failure or retry exhaustion
126 The customer command could not be started

On Unix, normal exits and terminating signals are propagated exactly after the customer process group and adopted descendants have been cleaned up.

Supervision policy

The compatibility default is never: one customer attempt is run. A strict, server-owned optional bootstrap block can select on_failure with either an attempt limit from 0 through 10 restarts or the schedule end. Unknown or malformed values fail closed to never. Restart delays use deterministic equal jitter, a one-minute cap, a five-minute stability reset, and a five-second schedule runway.

Non-default restart policy is currently limited to exact applications selected by the Liskov control plane through LISKOV_CARGO_SUPERVISION_CANARY_JSON. The variable is an internal fail-closed canary control, is removed before customer startup, and is not a customer-authored policy surface. Managed Runtime SSH receives its exact-job connector credential in the authenticated runtime-bootstrap response and takes it out of that in-memory response before customer startup. Both spellings of the bootstrap secret (LISKOV_BOOTSTRAP and the no-longer-read PROOF_SLIPWAY_BOOTSTRAP), the supervision canary control, and the reserved Runtime SSH environment credential are removed from the captured customer environment. Runtime values cannot reintroduce those protected names. The Lockbox bootstrap value is not one of them: it is fetch metadata rather than a secret, so it stays deliverable and visible.

The customer command inherits the processor's Android process environment, and four of those names are repaired before startup. They are repaired only when the value would be invalid inside the Debian rootfs. TMPDIR is kept if it names a directory the guest can write to, and otherwise becomes /tmp, which is created 1777 if missing. LD_PRELOAD is kept only while every object it names by path exists as a file in the guest. The loader fails the whole list, so one missing object removes the variable. PATH defaults to /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin when it is unset or empty, and HOME defaults to /root when unset. The repair runs on the inherited values before the signed runtime environment is merged. A signed value for any of these names therefore outranks both the inherited value and the default, and is never itself repaired. No other inherited name is touched. When anything was repaired, one runtime.environment.normalized supervisor diagnostic names each changed variable and the reason, never a value.

For bounded release canaries whose Shell host does not retain stderr, --diagnostic-exit-codes replaces status 70/75 with a non-secret stage code. This flag is an internal canary interface, not a customer compatibility surface:

Status Stage
80–82 Bridge setup or deployment identity
83–84 Deployment public key
85–86 Assigned-processor binding
87–88 Deployment signing
89 Request construction
90 Permanent server rejection
91–92 Response validation or binding
93–94 Runtime randomness or clock
95 Retry exhaustion
96 Runtime-environment endpoint construction
97–98 Runtime-environment randomness or clock
99 Runtime-environment signing
100 Runtime-environment request serialization
101 Runtime-environment transport
102 Runtime-environment server rejection
103–104 Runtime-environment response validation or binding

The flag changes only failure reporting: contact remains fail closed and the customer command is never started after an error.

Liskov-controlled canaries may also use the hidden --bridge-probe flag. It first reproduces Acurast's Cargo-native example call: signer_publicKey with curve: "p256". That first connection receives the remainder of the shared 15-second budget and is followed by the example's deployment_assignedProcessors call. The probe then checks unique decimal UInt request IDs and the helper's incompatible long request ID across Liskov's stricter Cargo bridge surface, including a domain-separated harmless Ed25519 signing call. It stops at the first decisive failure. The long-ID comparison runs last and is evidence only. The probe emits only method, ID-style, closed outcome, and optional numeric JSON-RPC code fields.

Retry boundary

Retryable bridge failures and incomplete identity replies use a 250 ms first delay and then 2-second intervals before the helper generates its nonce and timestamp. The helper signs once per process and reuses the exact serialized request for all HTTP attempts. Identity discovery and HTTP contact each allow at most 30 attempts, share one 60-second elapsed-time ceiling, and each HTTP attempt has a 10-second timeout.

Pre-contact reporting is independent of this retry loop. The started and terminal reports each receive one HTTP attempt, a two-second timeout, an 8-KiB response limit, and strict response binding.

Release

Each release publishes:

liskov-runtime-contact-v<VERSION>-aarch64-unknown-linux-musl
liskov-runtime-contact-v<VERSION>-aarch64-unknown-linux-musl.tar.gz
liskov-dropbear-2026.94-v<VERSION>-aarch64-unknown-linux-musl
liskov-dropbearkey-2026.94-v<VERSION>-aarch64-unknown-linux-musl
runtime-contact-release.json
SHA256SUMS

The raw helper, the two static Dropbear executables, and release-manifest v2 are attested separately. The manifest binds the immutable tag and source commit to every exact digest and byte size. The archive contains the helper and its fixed sibling liskov-dropbear and liskov-dropbearkey executables, plus this README and the FSL-1.1-Apache-2.0 license. Runtime access verifies all three executable digests before use and never installs packages inside the customer image. Verify the checksums before use:

sha256sum --check SHA256SUMS

Development

The repository uses Rust 2024 with MSRV 1.85. Validation is offline:

cargo fmt --all --check
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo build --workspace --all-targets --locked
cargo test --workspace --all-features --locked

Stable publication requires the repository's native AArch64 and emulated Cargo/PRoot release gates in addition to these host checks.

Local Acurast-shaped QEMU/PRoot runtime

Developers with Docker can run an AArch64 candidate inside the exact maintained Liskov Debian rootfs without installing QEMU or PRoot on the workstation:

scripts/run-qemu-proot-runtime.sh --self-test
scripts/run-qemu-proot-runtime.sh -- /workspace/path/to/aarch64-candidate --version
scripts/run-qemu-proot-runtime.sh --profile netlink-denied --self-test
scripts/run-qemu-proot-runtime.sh --no-shim -- \
  /workspace/tools/qemu-proot/managed-access-smoke.sh \
  /workspace/candidate/liskov-dropbear \
  /workspace/candidate/liskov-dropbearkey \
  /workspace/candidate/test-ld-linux \
  /workspace/candidate/test-libs \
  /workspace/candidate/test-openssh \
  /workspace/candidate/test-ssh-keygen \
  /workspace/candidate/test-nc

The wrapper bind-mounts the invocation directory read-only at /workspace and caches only the digest-verified rootfs under .cache/qemu-proot/. It runs the container as the invoking non-root UID, keeps privileged-port binding disabled, disables container SELinux relabeling so source files stay unchanged, and gives the container only SYS_PTRACE, which PRoot needs. The inner command builds the clean termux/proot v5.1.107.72 base identified by the Acurast processor 1.27.0-rc1 APK and uses the APK's PRoot shape: fake root, --kill-on-exit, --link2symlink, the /dev, /proc, /sys, and /dev/pts binds, /root as its working directory, and the documented PATH and HOME. The Liskov getifaddrs loopback shim is preloaded by default, as it is in the generated Cargo launcher.

The managed-access smoke uses the release Dropbear binary and production hardening flags. For that smoke only, it gives QEMU an unopenable argv[0] so Dropbear takes its built-in straight-fork fallback: QEMU user mode cannot redispatch Dropbear's per-connection /proc/self/fd AArch64 re-exec. Native AArch64 execution retains the normal per-connection re-exec behavior.

processor-like leaves route netlink available. That matches Acurast's current documentation, which says PRoot can expose Android's real interfaces through netlink. netlink-denied is a deliberate seccomp fault profile which returns EPERM only for socket(AF_NETLINK, ...) while leaving IPv4/TCP available. It is useful for proving a constrained tailscaled patch or tailscale-rs mode, but it is not a claim that every processor denies netlink.

This is a compatibility harness, not an Android security emulator. QEMU changes CPU execution, upstream PRoot replaces Acurast's patched Android libproot, and Docker cannot reproduce the processor app UID, SELinux policy, abstract bridge services, Android interfaces, or Acurast's outbound connect interceptor. The final processor canary remains required. Do not put auth keys in argv; provide a mode-0600 file below the selected workspace and let the candidate consume the file.

The managed-access smoke injects the same pinned static Dropbear and keygen companions as the release bundle plus the native ARM runner's test-only stock OpenSSH/netcat binaries, loader, and exact shared-library closure. It confirms port 22 is denied, starts the fixed loopback port-2222 Dropbear command, and uses stock OpenSSH through a local byte relay with a pinned host key. It performs no package-manager operation inside the maintained runtime image. The stock client bundle is not a release asset. The smoke also confirms killing the access sidecar does not change an independently running customer's exact exit.

Opt-in Tailscale SaaS Runtime SSH gate

After the offline and emulator profiles pass, operators with an approved customer-owned test tailnet can exercise official Tailscale control, DERP, and native SSH without paying for an Acurast job:

scripts/test-tailscale-saas-runtime-ssh.sh \
  --yes-provider \
  --credential-file /protected/path/tailscale-canary.json

The credential file parent must be mode 0700 and the file mode 0600. The JSON contract is kind, tailnet, tag, oauthClientId, and oauthClientSecret; the gate accepts only kind tailscale and tag tag:liskov-runtime. It refuses a local Tailscale client which is offline or logged into a different tailnet and never changes the local login.

The gate verifies the pinned official ARM64 archive, mints one tagged, preauthorized, ephemeral, non-reusable 15-minute key, supplies it only through a mode-0600 file, and removes that file immediately after authentication. It then finds exactly one non-baseline tagged device, invokes tailscale ssh with an argument array, verifies root and maintained-rootfs markers, stops the exact named emulator container, and deletes only the exact persisted key and device. Output is limited to booleans and counts; OAuth values, auth keys, tailnet names, hostnames, device IDs, network maps, daemon logs, and SSH output remain inside protected disposable state and are removed on exit.

This is a provider-real compatibility test, not the Android release gate. It does not reproduce the processor app UID, SELinux policy, Android interfaces, or Acurast's private outbound-connect interceptor, and it does not prove customer-workload continuity through a sidecar failure.

Customer-secret installation

Runtime-contact 0.10.40 consumes the optional signed-bootstrap v2 secrets.customerRequired hint. It retrieves the exact job-bound customer secret group before starting the customer command, independently of logging. Required delivery or installation failure exits with code 70 and a redacted runtime-secret diagnostic. Older bootstrap responses keep their existing behavior.

Environment values retain their exact strings. Absolute file destinations are installed as private 0600 files using directory descriptors, no-follow opens, staging and rollback. Environment changes follow successful file installation. Symlinks, traversal and reserved staging names are refused. Interrupted file replacements are recovered on the next start; release receipts alone do not prove process installation. Existing artifacts must be rebuilt to use this helper.

Authorized network coverage samples

Coverage authorizations may opt the pinned native helper into networkSample version 1 using networkProberUrl. Only the compiled HTTPS prober origin is accepted. No authorization means no network sampling. The prober capability is opaque; the helper never receives its HMAC key.

The complete sample has a 30-second cancellation deadline, decimal-MB transfer caps, no measurement retries and closed errors. UDP runs over IPv4 before bulk traffic; HTTPS tests both address families. Raw addresses are never serialized. The largest completed directional leg supplies throughput; sequence/timing records supply RTT, RFC3550 jitter and loss. A near-expiry authorization skips the sample to preserve the existing result-delivery budget. The coverage job window remains 120 seconds until live measurements support changing it.

The same authorization also enables inboundReachability version 1: the helper binds one port in 20000-60000 across both families, asks the prober to dial it once per family, and signs the 32-byte nonce that arrives over that inbound connection. The signature is written back down the same socket, because a signature that did not travel the measured path proves nothing about it. The listener is closed when the check ends and there is no persistent port.

The check has its own 4-second budget, separate from the sample's 30 seconds, so a slow link is never the reason its own reachability went unmeasured. Every wait is two seconds and nothing is retried: a firewalled processor is the normal outcome and costs what a reachable one costs. The helper carries the prober's signed verdict verbatim and never writes one; it cannot, and it never learns which address the prober dialled.

The signed block, shared vector and catalog match the liskov-rs contract owner. Existing envelopes omit the optional blocks and retain their canonical bytes. Refs BKLG-20260914-fglr; ADR-0139.

License

From v0.11.0 this repository is licensed under the Functional Source License, Version 1.1, with Apache-2.0 as the future licence (FSL-1.1-Apache-2.0): each version becomes available under Apache-2.0 two years after its release. Every release tag before v0.11.0 remains under Apache-2.0. See LICENSE.

Application service discovery (candidate)

The job-facing liskov.discovery.v1 contract remains part of the unreleased V6 private endpoint path. It does not activate V6 or create a tailnet. The application declares its named HTTP service and local port; Liskov returns observed publication URLs. A hostname is not proof of peer reachability.

For a Tailscale workload with declared endpoints the helper exports LISKOV_DISCOVERY_FILE, LISKOV_PEER_PROXY (a loopback socks5h:// proxy), and LISKOV_RUNTIME_INSTANCE_ID. The file appears after the first successful signed health refresh. It is atomically replaced; unavailable or invalid responses preserve the previous snapshot and its original timestamp. The snapshot contains schema, applicationUid, self, opaque revision, observedAtMs, and peers[].{jobId,instanceId,services}. Named services contain protocol and named endpoints with provider, URL (nullable), observed state and observation timestamp. Limits are 256 peers, 32 endpoints per peer, 512 KiB. Health diagnostics include discovery version, last-success time and refresh failure, allowing operators to distinguish an empty directory from a stale one. The proxy is not enabled for SSH-only attachments. HTTP/model/global proxy variables are not modified. Applications retain their own authentication.

Direct IPv6 discovery for native workloads (release candidate)

For jobs without the v1 Tailscale publication path, the helper supplies LISKOV_RUNTIME_INSTANCE_ID, LISKOV_PEER_ADVERTISEMENT_FILE, and LISKOV_PEER_DIRECTORY_FILE before starting the native workload. It does not supply LISKOV_DISCOVERY_FILE in this mode. This is helper-owned runtime metadata and does not change V5 policy or its authored variables. A missing advertisement file means the workload has not opted in.

To opt in, atomically replace the advertisement file with one JSON object no larger than 4 KiB:

{
  "schema": "liskov.peer-advertisement.v2",
  "service": "sync",
  "port": 8841,
  "ready": true,
  "tlsFingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}

The service uses HTTPS in this version. The fingerprint is SHA-256 of the listener's leaf certificate DER. The helper also accepts optional peerSyncLastSuccessAtMs and peerSyncFailureCode (none, connect, tls, auth, identity, exchange, or discovery). These sync summaries are application-reported, not proof of platform delivery or peer reachability. It refuses extra fields, symlinks, oversized files, invalid ports, and invalid fingerprints. The helper supplies verified app/job/runtime identity itself; the workload cannot advertise an IP, URL, credential, or identity. An attempted direct advertisement on a v1 Tailscale job reports a conflict and retains v1 discovery.

A malformed, oversized, or symlinked local file still produces a signed v2 health beat with discoveryVersion: 2 and only a bounded peerAdvertisementError in place of endpoint attributes. Liskov can retain that refusal and return typed unavailable; the helper cannot write a complete directory from this beat or extend the last good lease. A missing file remains no opt-in, while a v1 Tailscale conflict keeps its v1 request.

Each health beat reads the current file and signs the v2 request attributes. A complete, current liskov.discovery.v2 response replaces the private directory file atomically. The snapshot includes a lease and each peer's expiry; consumers must stop using expired endpoints. A failed, stale, unsupported, or oversized response leaves the last good bytes and original expiry intact and reports a bounded failure code. The helper removes the advertisement before each supervised child start, so a restarted child must opt in again. Direct discovery is advisory; applications still authenticate peers and pin each advertised TLS certificate.

On child exit the helper stops reporting readiness and removes the input. An advertisement already accepted by the server retains only its original lease, at most 90 seconds from that health beat; the exited child's file cannot renew it.

discoveryLastSuccessAtMs retains the v1 meaning: it is the server's observedAtMs on the last accepted snapshot, rather than a local receive timestamp. The helper uses that value to reject delayed snapshots.

Application readiness

A manifest opts in with the signed runtime-env literal LISKOV_APPLICATION_READINESS=v1. The helper reads that name only from the signed map. Any other value, including the same name in the inherited processor environment, leaves health beats and the child environment unchanged.

When the literal matches, the helper gives the child LISKOV_APPLICATION_READY_FILE: an absolute path in the same private 0700 directory as LISKOV_PEER_ADVERTISEMENT_FILE. A forged ready-file path in the signed map is replaced by that path. The child creates the file when it can serve. The helper removes it before each supervised child start, so a restarted child is pending until it writes the file again.

Every runtime.health then carries one scalar, applicationReadiness. The value is pending until a regular file of at most 4 KiB is at that path, and ready after. The helper ignores the bytes. A missing file, a symlink, a FIFO, or an oversized file stays pending, and a symlink is not followed.

About

Static Rust first-contact helper for Liskov-managed Acurast Cargo/PRoot workloads

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages