# Package format

> The .sapp container — ZIP layout, signature block, key rotation, Merkle verification, delta updates and the SBOM.

Source: https://miniapp.sollar.com/reference/package-format/

---

A mini app ships as a single `.sapp` file. It is a ZIP archive with a signature block inserted
before the central directory, and it borrows its structure from formats that have already survived
adversarial contact: Android's APK Signature Scheme, Chrome's CRX3, and fs-verity.

## Layout

```
┌─────────────────────────────────────────────┐
│ ZIP entries                                 │
│   manifest.json          ← covered by sig   │
│   index.html                                │
│   assets/…                                  │
│   sbom.cdx.json                             │
├─────────────────────────────────────────────┤
│ Sollar signature block                      │
│   • signer chain (lineage)                  │
│   • Ed25519 signature                       │
│   • ECDSA P-256 signature                   │
│   • Merkle root over content                │
│   • countersignature (store or org CA)      │
├─────────────────────────────────────────────┤
│ ZIP central directory                       │
│ End of central directory                    │
└─────────────────────────────────────────────┘
```

Placing the signature block *before* the central directory is the APK v2 arrangement. It means the
signature covers the archive's bytes rather than its parsed entries, which closes the class of
attack where a verifier and an extractor disagree about what the archive contains.

## What each borrowed idea buys

| Problem | Solution | Precedent |
|---|---|---|
| Verifier and extractor parse differently | Sign the byte range, not the entry list | APK Signature Scheme v2 |
| Signing key must be replaced without breaking installs | Signer lineage: the old key signs the new key's authority | APK Signature Scheme v3 |
| Whole-file hash means reading the whole file before first paint | Merkle tree — verify each block as it is read | APK v4 / fs-verity |
| One algorithm breaks and everything installed is unverifiable | Two independent signatures, both required | CRX3 |
| An attacker publishes under someone else's identifier | The app `id` is derived from the public key | CRX3 |
| A dependency turns out to be vulnerable and nobody knows who shipped it | CycloneDX SBOM inside the package | — |

## Signing

Two signatures, both required, over the same content:

- **Ed25519** — fast, small, no parameter choices to get wrong.
- **ECDSA P-256** — broad hardware-backed keystore support on both platforms.

A package that verifies under only one is refused. This is CRX3's reasoning: the cost of carrying a
second algorithm is a few hundred bytes, and the benefit is that a break in either curve does not
strand every installed app.

```sh
sollar build                     # produces dist/com.acme.erp-2.4.1.sapp
sollar sign --key <keyref>       # adds the signature block
```

> **DANGER**
The signing key never goes in a file, a repository, a CI variable, or an environment file. Pass a
reference to a secret manager or a hardware token; `sollar sign` resolves it at the moment of use
and never writes it to disk.

### Keyless signing

`sollar sign --keyless` uses Sigstore: an ephemeral key, an OIDC identity, and a transparency-log
entry. Nothing to store, nothing to leak, and a public record of who signed what. Available for both
channels; recommended for CI.

### Countersignature

The developer signature proves authorship. It is not enough to install.

| Channel | Countersigned by |
|---|---|
| `store` | The Sollar store key, after review |
| `tenant-private` | The organisation's CA |

The client refuses a package with no countersignature from one of the two. Tenant-private apps skip
Sollar's *review*; they do not skip the *cryptography*.

## Key rotation

The signer lineage is a chain: each new key is authorised by a signature from the previous one. A
client that has installed version 2.4.1 signed by key A will accept 2.5.0 signed by key B, because
B's authority is proved by A within the package.

Losing a signing key with no lineage entry means the app cannot be updated by anyone, including you.
Rotate deliberately, and keep the lineage.

## Updates

Full and delta, both supported.

- **Delta** — a bsdiff patch against a specific prior version. The client falls back to a full
  download when it holds a version with no patch path.
- **Merkle verification** — the client verifies blocks as it reads them, so a mini app starts before
  the whole package is hashed.
- **Monotonic versions** — the client accepts only a higher version than the one installed. This is
  what makes rollback work as a *new version*, not a reversal; see [Distribution](/platform/distribution/).

## Limits

| | Limit | Note |
|---|---|---|
| Package size | 8 MB | The main package; enforced at submission |
| Total with subpackages | 24 MB | Lazily loaded parts |
| Single file | 4 MB | |
| Manifest | 256 KB | |
| `actions[]` | 64 per app | An agent given hundreds of tools chooses badly |
| `network.connect` | 32 hosts | A list longer than this is not an allowlist |

Size limits exist because a mini app that takes as long as an app-store download has lost the only
advantage it had.

## SBOM

`sbom.cdx.json`, CycloneDX 1.7, generated at build:

```sh
sollar build --sbom
```

Required for the store channel, recommended for tenant-private. The submission gate scans it against
known-vulnerability data, and a high-severity match in a shipped dependency blocks the release.

The SBOM is what makes the question "which mini apps ship the vulnerable version of this library"
answerable in minutes rather than by asking every developer.

## What is *not* in the package

**No remote code.** The Content Security Policy floor is `script-src 'self'` with no remote host and
no `'unsafe-eval'` — every executable byte arrives inside the verified package.

This is a security property and a legal one. Google Play's Device and Network Abuse policy exempts
from its downloaded-code prohibition anything running *"in a virtual machine or an interpreter where
either provides indirect access to Android APIs (such as JavaScript in a webview or browser)"*. A
mini app pulling a script from a CDN is downloading executable code from outside the verified
package, which is exactly what the exemption does not cover. See [Google Play](/compliance/google-play/).

A third-party resource that genuinely must be remote requires Subresource Integrity with a pinned
hash.
