Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/export-share-invitation-descriptor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.97.1

- name: Test the public descriptor exporter
run: cargo test -p tinycloud-node --bin export-share-invitation-descriptor
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ jobs:
fetch-depth: 0
token: ${{ secrets.RELEASE_PLZ_TOKEN }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install reviewed Rust toolchain
uses: dtolnay/rust-toolchain@1.97.1

- name: Run release-plz
id: release_pr
Expand Down Expand Up @@ -80,8 +80,8 @@ jobs:
fetch-depth: 0
token: ${{ secrets.RELEASE_PLZ_TOKEN }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install reviewed Rust toolchain
uses: dtolnay/rust-toolchain@1.97.1

- name: Run release-plz release
uses: release-plz/action@v0.5
Expand Down
30 changes: 26 additions & 4 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ jobs:
- name: Checkout TinyCloud repository
uses: actions/checkout@v4

- name: Install reviewed Rust toolchain
uses: dtolnay/rust-toolchain@1.97.1
with:
components: clippy, rustfmt

- name: Add the wasm32 target
run: rustup target add wasm32-unknown-unknown

Expand Down Expand Up @@ -65,6 +70,11 @@ jobs:
- name: Checkout TinyCloud repository
uses: actions/checkout@v4

- name: Install reviewed Rust toolchain
uses: dtolnay/rust-toolchain@1.97.1
with:
components: clippy, rustfmt

# TC-381: this job used to run `cargo test -p tinycloud-core postgres_`.
# libtest exits 0 when a name filter matches zero tests, and all four
# targets returned early when TINYCLOUD_TEST_POSTGRES_URL was unset — so
Expand Down Expand Up @@ -106,6 +116,11 @@ jobs:
- name: Checkout TinyCloud repository
uses: actions/checkout@v4

- name: Install reviewed Rust toolchain
uses: dtolnay/rust-toolchain@1.97.1
with:
components: clippy, rustfmt

- name: Add the wasm32 target
run: rustup target add wasm32-unknown-unknown

Expand All @@ -121,6 +136,11 @@ jobs:
- name: Checkout TinyCloud repository
uses: actions/checkout@v4

- name: Install reviewed Rust toolchain
uses: dtolnay/rust-toolchain@1.97.1
with:
components: clippy, rustfmt

- name: Fmt
run: cargo fmt --all -- --check

Expand All @@ -129,12 +149,14 @@ jobs:
steps:
- name: Checkout TinyCloud repository
uses: actions/checkout@v4
- name: Install reviewed Rust toolchain
uses: dtolnay/rust-toolchain@1.97.1
with:
components: clippy, rustfmt
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Ensure rustfmt is available
run: rustup component add rustfmt
- name: Verify generated capability artifacts match capabilities.json
run: node scripts/gen-capabilities.mjs --check
- name: Verify deployed Policy/v3 probe contract
Expand All @@ -151,9 +173,9 @@ jobs:
steps:
- name: Checkout TinyCloud repository
uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.97.1
with:
components: rustfmt
components: clippy, rustfmt
- uses: actions/setup-python@v5
with:
python-version: "3.12"
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ ARG RUNTIME_BASE=scratch
# Optional: pass "dstack", "duckdb", or "dstack duckdb" to enable build features.
ARG CARGO_FEATURES=""

FROM rust:alpine AS chef
FROM rust:1.97.1-alpine AS chef
RUN apk add --no-cache musl-dev pkgconfig openssl-dev openssl-libs-static g++ perl make
RUN test "$(rustc -V | awk '{print $2}')" = "1.97.1"
RUN cargo install cargo-chef
WORKDIR /app

FROM chef AS planner
COPY ./Cargo.lock ./
COPY ./Cargo.toml ./
COPY ./rust-toolchain.toml ./
COPY ./tinycloud-node-server/ ./tinycloud-node-server/
COPY ./tinycloud-auth/ ./tinycloud-auth/
COPY ./tinycloud-core/ ./tinycloud-core/
Expand All @@ -27,6 +29,7 @@ RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
ARG CARGO_FEATURES=""
COPY --from=planner /app/recipe.json recipe.json
COPY --from=planner /app/rust-toolchain.toml ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
if [ -n "$CARGO_FEATURES" ]; then \
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ With telemetry enabled, backend time-to-first-byte is recorded under the `server

## Quickstart

To run TinyCloud Protocol locally you will need the latest version of [rust](https://rustup.rs).
To run TinyCloud Protocol locally, install [Rustup](https://rustup.rs). The
repository selects the reviewed Rust `1.97.1` toolchain automatically through
`rust-toolchain.toml`; do not substitute a floating `stable` compiler.


You will need to create a directory for TinyCloud Protocol to store data in:
Expand Down
119 changes: 40 additions & 79 deletions deploy/share-email/README.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,40 @@
# TinyCloud Node share-email deployment

This deployment consumes Share contract commit
`36f6c4303eca3bee917692c77237c264b4dfa342` and manifest digest
`pl8-1Rpx_DYCBjOpK3hRrLfrSVDINNFssZDfFw6BMTs`. A different digest or an
ancestor-only pin is a release failure.

tinycloud.toml.example is the checked-in, non-secret configuration shape for
an enabled exact-email node. Copy it out of the repository, fill in the
operator-delivered paths and mount it with TINYCLOUD_CONFIG_FILE. The single
mounted trust-bundle path is the only production source for the public trust
tuple; missing or inconsistent legacy field overrides fail closed. Never put a
private key, database password, claim, credential, or token in the file.

## Delivering the trust bundle (TC-397)

The trust document has two interchangeable delivery forms, and exactly one may
be configured — setting both is a startup error, because two sources for one
document is the divergence the shared bundle exists to prevent.

- `trust_bundle_path` / `TINYCLOUD_SHARE_EMAIL__TRUST_BUNDLE_PATH` — a
read-only mounted file. Use this wherever a host filesystem exists.
- `trust_bundle_base64` / `TINYCLOUD_SHARE_EMAIL__TRUST_BUNDLE_BASE64` — the
same bytes, base64-encoded, inline in the environment.

The inline form exists because the dstack/Phala target admits nothing else.
The release image's runtime stage is `FROM scratch`, so there is no shell and
no `base64` binary — the decode-to-tmpfs entrypoint `share-api`'s compose file
uses cannot be reproduced here — and a Phala deployment uploads only a compose
file, so there is no host path to bind-mount a bundle from. An opaque
environment variable is the one channel that reaches the container. It is
base64 rather than raw JSON because Figment's `Env` provider interprets brace-
and bracket-delimited values as structured data; a base64 token passes through
Figment, YAML and dstack's sealed environment storage byte-for-byte.

Produce it from the reviewed document without a trailing newline or line
wrapping:

```sh
SHARE_TRUST_BUNDLE_BASE64="$(base64 < trust-bundle.production.json | tr -d '\n')"
```

`share-api` reads the same document from a variable of the same name and in the
same encoding, so a single sealed value can feed both services and cannot drift
between them.

### `emailOrigin`

The document carries an `emailOrigin` field that Share's schema requires (it
feeds the CSP `connect-src` without which the browser blocks the send). The
node validates it — canonical HTTPS origin, no path, query, fragment, port or
credentials, and covered by the production placeholder scan — but does not
consume it, exactly like `shareOrigin` and `registryOrigin`. It is optional on
this side so that adding it did not become a breaking change to an unchanged
document version; the requirement is enforced by Share, its only consumer.
Unknown fields are still rejected.

The staging compose file consumes that mounted config and has no development
or test fallback. It requires an immutable image reference, a PostgreSQL URL,
the CA bundle, issuer and invitation public keys, the signed authority bundle,
and the node key source. The node then refuses startup when any of these are
partial or inconsistent:

- `allowed_origins` is exactly `https://share.tinycloud.xyz`; wildcard CORS is
never accepted for the share routes.
- issuer DID, `opencredentials.email/v1`, issuer `kid`, key version, and
public key form one pinned trust tuple.
- invitation `kid` and public key match the node signer derived from
`TINYCLOUD_KEYS_SECRET`.
- the authority bundle contains cryptographically verified policy and
enforcement material, enrollment, two fresh status observations, and a
current runtime attestation.
- PostgreSQL uses `sslmode=verify-full` and the configured CA bundle exists.
- the database transaction and all signed evidence pass the startup readiness
probe before `/info` advertises `share-email-claim`.

The mounted fixture uses the same production composition and derives its node
signer from the configured key secret. Its generated authority artifacts are
test data only and are never accepted by this deployment template.
# TinyCloud Policy/v3 delivery trust

This directory documents the node-side trust configuration used by native
sharing. The node stores and serves the owner's content, registers Policy/v3,
authorizes recipient invocations, and signs a narrowly scoped email-delivery
receipt. It does not upload share blobs or delegate content authority to an
email service.

`tinycloud.toml.example` is a non-secret configuration shape. Supply exactly
one copy of the reviewed trust bundle either as a read-only file through
`trust_bundle_path` or as base64 through `trust_bundle_base64`. Configuring
both is a startup error. Private keys, database passwords, credentials, and
delivery tokens do not belong in this file.

The durable production hand-off is the
[production trust-bundle contract](production-trust-bundle-contract.md). Share
owns the reviewed bundle after Share#102 removes its former copy; Node owns the
fail-closed reader and must receive the same bytes through the documented
environment contract before a release can boot.

The trust bundle must bind these production origins:

- `shareOrigin`: `https://share.tinycloud.xyz`
- `registryOrigin`: `https://registry.tinycloud.xyz` (node discovery only)
- `emailOrigin`: `https://witness.credentials.org`
- the exact owner-node origin and node/enforcer identities

`emailOrigin` is the separately validated audience of the short-lived,
single-use generic credential-invitation authorization. It currently equals
the OpenCredentials issuer origin because that origin receives
`POST /v1/credential-invitations`; the Node does not infer it from credential
issuance metadata. The node validates the requested recipient,
share URL, label, issuer, audience, expiry, and JTI against the registered
delegation before it signs. The email delivery service can then send that exact invitation;
it cannot mint policy, read content, proxy an invocation, or receive a bearer
fragment.

The production node still fails closed on inconsistent origins, keys,
attestation, authority material, or PostgreSQL TLS configuration. The mounted
fixture uses the same validation with test-only authority artifacts.
68 changes: 68 additions & 0 deletions deploy/share-email/production-trust-bundle-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Production share-email trust-bundle contract

This is the durable hand-off contract for the reviewed public trust bundle. It
contains no private key material. Share owns the reviewed JSON artifact after
Share#102; TinyCloud Node consumes exactly one copy and refuses to start the
share-email capability if its fields do not meet this contract.

## Required fields

The JSON version is `tinycloud.share-email-trust-bundle/v1`. Its production
origins are exact strings:

- `shareOrigin` and `returnOrigin`: `https://share.tinycloud.xyz`
- `registryOrigin`: `https://registry.tinycloud.xyz`
- `credentialsOrigin`: `https://witness.credentials.org`
- `emailOrigin`: `https://witness.credentials.org`

`emailOrigin` is the independently checked generic invitation-delivery
audience, currently co-located with credential issuance at
`https://witness.credentials.org`. It is not a route served by Node and must
match the origin receiving `POST /v1/credential-invitations`.
Node exposes Policy/v3 admission/control and ordinary `/delegate` and `/invoke`
data-plane routes only; it does not expose `/share` routes or proxy delivery.

For addressed delivery, Share sends the SDK's `sealedEnvelope` and
`envelopeKey` request fields. The recipient-bearing, owner-signed envelope is
AES-256-GCM sealed; its CID addresses `/s/<cid>` and its envelope key is kept
only in `#k=<key>`. This key unwraps share-envelope metadata, not document
content. Node rejects plaintext recipient envelopes in a query or path and
checks that the sealed CID, decrypted canonical envelope, exact recipient, and
delivery authorization all agree.

The node identity must be internally exact, not merely a canonical DID:

- `nodeAudience` is `did:web:<nodeOrigin host>`;
- `nodeInvitationKid` is
`<nodeAudience>#invitation-key-<nodeKeyVersion>` and the version is positive;
- `nodeInvitationPublicKey` exactly equals the public descriptor derived by the
production Node `TINYCLOUD_KEYS_SECRET`; and
- `nodeEnabled` is `true`.

The issuer identity is exact: `issuerDid` is
`did:web:issuer.credentials.org`, `issuerVct` is
`opencredentials.email/v1`, `issuerKid` belongs to that DID, its key version is
positive, its public key is canonical, and `issuerEnabled` is `true`.

The separately owner-signed authority material must bind every Policy/v3
`enforcerDid` to the same Node `nodeAudience`; the enforcer binding signature
is checked against that Node's derived attestation key at registration. This
keeps the concrete enforcer identity coupled to the concrete Node identity
without copying a deployment-specific DID into source control.

## Delivery and release hand-off

Share serializes the reviewed JSON compactly and base64-encodes it without
line wrapping. Before a Node release, the release owner places that exact value
in the GitHub secret `PROD_TINYCLOUD_SHARE_TRUST_BUNDLE_BASE64`. The deploy
workflow passes it as `SHARE_TRUST_BUNDLE_BASE64`; the Node runtime consumes it
as `TINYCLOUD_SHARE_EMAIL__TRUST_BUNDLE_BASE64`. A mounted deployment instead
sets `TINYCLOUD_SHARE_EMAIL_TRUST_BUNDLE` to the reviewed JSON file and uses
`trust_bundle_path`.

Do not set both sources. Do not create a substitute `api.share.tinycloud.xyz`
or `email.tinycloud.xyz` audience: Node rejects it in non-fixture builds.
Share must emit the generic OpenCredentials origin before it removes or
rotates its legacy bundle. Node validates this contract before
advertising share-email readiness, so a missing, malformed, mismatched, or
fixture bundle fails closed.
5 changes: 2 additions & 3 deletions deploy/share-email/tinycloud.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ enabled = true
# version, and public-key fields below. No split trust environment variables
# are accepted by the production compose file.
trust_bundle_path = "/run/tinycloud/share-email-trust-bundle.json"
# TC-397: where a file cannot be mounted at all — the release image is
# `FROM scratch` and a dstack/Phala deployment uploads only a compose file —
# the same document arrives inline instead, base64-encoded, through
# Where a file cannot be mounted, the same document can arrive inline,
# base64-encoded, through
# TINYCLOUD_SHARE_EMAIL__TRUST_BUNDLE_BASE64 (config key `trust_bundle_base64`).
# Set exactly one of the two. Configuring both is a startup error: two sources
# for one document is precisely the divergence this bundle exists to prevent.
Expand Down
8 changes: 8 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[toolchain]
# Keep local development, CI, and release builds on the reviewed compiler
# version. Do not use a floating `stable`: new deny-by-default lints can turn
# an otherwise unchanged release head red.
channel = "1.97.1"
profile = "minimal"
components = ["clippy", "rustfmt"]
targets = ["wasm32-unknown-unknown"]
Loading
Loading