Skip to content
rev3rse

Documentation

Integrate IP Vault

Declare your assets in one file. Your CI seals them on its own runner and registers their keys; your build packages the sealed files; a check after the build proves nothing shipped in the clear; the SDK opens them in your app, offline, for licensed installations only.

Early access. Self-serve signup, which connects your CI, is not open yet; until it is, we connect design partners’ repositories ourselves (talk to us). The scan at the end of this page needs no account.

Overview

How it fits together

  1. Begin. The release step asks the vault service for a new key version per asset. The service allocates them (a reserved version is never handed out again) and returns your tenant’s ingestion public key.
  2. Seal, on your runner. Each asset gets a fresh key, is sealed to its output path, and its key is wrapped to the ingestion key. The raw key never touches disk; the plaintext never leaves the runner.
  3. Register. The wrapped keys and the sealed files’ hashes are committed in one transaction, and grants are added to the entitlements you named. Grants are only ever added, so licences for builds you already shipped keep working.
  4. Build and check. Your build packages the sealed files. The check fails the build if one is missing or a plaintext input is in the package.
  5. Open. The SDK activates each installation once, with an activation code or a voucher from your backend, and opens assets in memory after that with no network.

Every step is idempotent. Repeating one resumes the same release with the same sealed files; a re-run of the CI job is a new attempt with fresh keys. Nothing is ever sealed twice under one key.

Step 1

Declare your assets

ipvault.toml, at the root of your repository, declares every asset. Models, datasets, maps, content and rules are all assets; a model additionally needs the SDK adapter for its runtime.

version = 1

[app]
platform = "android"
id = "com.example.app"

[[asset]]
id = "model:classifier"                     # what your app opens
version = 3                                 # your version of the asset
source = "private/classifier.ort"           # plaintext: never inside a packaged directory
output = "app/src/main/assets/classifier.ipva"
entitlements = ["premium"]                  # licences of these entitlements can open it

[[asset]]
id = "data:tiles"
version = 1
source = "private/tiles.bin"
output = "app/src/main/assets/tiles.ipva"
entitlements = ["premium"]
mode = "H"                                  # import the key into the Keystore where the device can
FieldMeaning
idStable asset id, what your app asks the SDK for. Required.
versionYour version of the asset. Required.
sourceThe plaintext file, relative to the manifest. It must not be inside a directory the manifest packages from: the release refuses it, so it cannot ship. Required.
outputWhere the sealed file is written, inside your app’s sources. Required.
entitlementsThe entitlements whose licences can open the asset; your CI credential must be allowed to add grants to each. Required.
mode"W" (default) or "H": with "H", a device that passes the secure-import self-test gets the key imported into its Keystore.
key_idDefaults to the asset id. The first app to publish a key id owns it.
content_type, seg_sizeMedia type of the plaintext, and bytes per sealed segment (a power of two). The defaults suit both device modes.

Keep the plaintext out of version control if you can: fetch it into the runner in an earlier step, from wherever you keep it. For a model, the runtime’s own in-place format (.ort for ONNX Runtime) leaves nothing of the model in the process after a session closes.

Step 2

Connect your CI

Publishing takes a credential for one app, scoped to the entitlements it was created for. It cannot read a key, replace a version that is already registered, remove a grant or touch another app. It expires, and revoking it takes effect on its next request.

  • OIDC, on GitHub and GitLab (recommended). Bind your repository to the app. The job then proves who it is with the short-lived token GitHub or GitLab signs for it, so there is no secret to store, rotate or leak. A binding names the repository and its owner by numeric id, which a rename does not change, and the refs that may publish (exact, or a prefix such as refs/tags/v*); it can also require an environment or a workflow.
  • A CI token, for any other runner. Store it as IPVAULT_TOKEN: a repository or environment secret on GitHub, a masked and protected CI/CD variable on GitLab. Tokens start with ipvk_, so secret scanners can recognise one that leaks.
  • Neither is an end-user credential, and neither goes into your app. Your app carries only public settings: the tenant, the service address and the licence verification key.

Step 3

GitHub Actions

name: release
on:
  push:
    tags: ["v*"]                  # publish from trusted refs only

jobs:
  android:
    runs-on: ubuntu-24.04
    environment: release          # optional: required reviewers; a binding can require it
    permissions:
      contents: read
      id-token: write             # the job's OIDC identity: no secret to store
    steps:
      - uses: actions/checkout@<commit-sha>

      # Seal on this runner and register the keys. Writes the sealed outputs.
      # Without OIDC, pass a CI token instead: with: token: ${{ secrets.IPVAULT_TOKEN }}
      - id: ipvault
        uses: rev3rse-dev/ipvault-release-action@<commit-sha>

      - run: ./gradlew assembleRelease

      # Fail the build if a sealed file is missing or a plaintext input shipped.
      - uses: rev3rse-dev/ipvault-release-action@<commit-sha>
        with:
          verify: app/build/outputs/apk/release/app-release.apk
          receipt: ${{ steps.ipvault.outputs.receipt }}
  • Pin the action by commit SHA. The commit pins the ipvault binary too: the action downloads it and refuses to run it unless its SHA-256 matches the value in the same commit.
  • Publish from trusted refs. The action refuses, by name, to publish from a pull request opened from a fork, and from pull_request_target, which runs with your secrets and identity next to code from the pull request. IP Vault itself refuses an OIDC token from a pull_request or pull_request_target job, whatever a binding says. The check and the scan need no credential and run anywhere.
  • Outputs: release-id, receipt (pass it to the check), passed, exit-code, report and junit. The job summary shows what was sealed and registered.

Step 3

GitLab CI/CD

Coming soon: CI/CD Catalog components that do this for you. Until they are listed, run the CLI in a job. It reads the job’s ID token, and derives the attempt’s name from the job.

ipvault-release:
  stage: .pre
  image: alpine:3.20                # pin it by digest
  rules:
    - if: $CI_COMMIT_REF_PROTECTED == "true"   # IP Vault publishes from protected refs only
  id_tokens:
    IPVAULT_ID_TOKEN:               # the job's identity: no secret to store
      aud: https://vault.rev3rse.dev
  script:
    - apk add --no-cache curl
    - v=0.1.0; t=x86_64-unknown-linux-musl
    - curl -fsSLO "https://github.com/rev3rse-dev/ipvault-cli/releases/download/v$v/ipvault-$v-$t.tar.gz"
    - echo "6851005905e89080eda704445ec621f1a6feee1cc4d73d76701ad5b8ce9454a8  ipvault-$v-$t.tar.gz" | sha256sum -c -
    - tar -xzf "ipvault-$v-$t.tar.gz" && mkdir -p ipvault-release
    - ./ipvault-$v-$t/ipvault release publish --json-out ipvault-release/receipt.json
  artifacts:
    paths: ["**/*.ipva", "ipvault-release/"]   # the build job packages the sealed files
  • IP Vault admits the ID token through your binding, from protected branches and tags only. A CI token in IPVAULT_TOKEN, a masked and protected variable, works too and takes precedence.
  • Check the package after the build the same way: ipvault release verify --package <apk> --receipt ipvault-release/receipt.json.
  • Self-managed GitLab: use a CI token. The hosted service accepts ID tokens from GitLab.com.

Anywhere else

Any other CI

The integrations run the same commands, which work on any runner with curl. Pass --operation-id outside GitHub and GitLab: it names the attempt, and repeating it resumes the attempt.

ipvault release plan                       # what ipvault.toml would release; no token, no network
export IPVAULT_TOKEN=...                   # a CI token from your secret store, never on the command line
ipvault release publish --operation-id "build-$BUILD_ID" --json-out receipt.json
./gradlew assembleRelease
ipvault release verify --package app/build/outputs/apk/release/app-release.apk --receipt receipt.json

Step 4

Open the assets in your app

Android 9 and later. The SDK is distributed with your account during early access.

dependencies {
    implementation("dev.rev3rse.ipvault:ipvault:<version>")
    // the adapter for your runtime, if the asset is a model:
    implementation("dev.rev3rse.ipvault:ipvault-onnxruntime:<version>")   // or ipvault-litert
    // optional: bind a Play Integrity verdict to each activation and renewal
    implementation("dev.rev3rse.ipvault:ipvault-playintegrity:<version>")
}
val vault = IPVault.open(context, VaultSettings(
    tenant = "<your tenant>",
    endpoint = "https://vault.rev3rse.dev",
    licenceKeys = listOf(LicenceKey(kid, publicKey)),   // public: GET /v1/licence-keys
))

// Every call blocks: run them off the main thread.
when (val s = vault.status()) {
    is VaultStatus.NeedsActivation -> vault.activate(code)   // or activateWithVoucher(...)
    is VaultStatus.Expired -> vault.renew()
    is VaultStatus.Unusable -> vault.activate(code)
    is VaultStatus.Active ->
        vault.session(File(filesDir, "classifier.ipva"), OnnxRuntime()).use { session ->
            val out = session.run(mapOf("input" to FloatTensor(shape, values)))
            // the app gets results; it never holds the model
        }
}
  • Who gets a licence is your decision, made with activation codes you issue, or with vouchers your own backend signs for a signed-in customer. Activation happens once per installation; renewal follows the lease.
  • Ending access. Revoke the licence when a subscription ends: renewal does not ask your subscription system by itself. The installation stops opening assets once its lease and offline grace run out.
  • Models run through a session: requests in, results out, the model loaded in place from the SDK’s memory and wiped when the session closes. Adapters exist for ONNX Runtime and LiteRT. Other data is read through an explicit, opt-in view.
  • Mode H (the manifest’s mode = "H") imports the key into the Keystore on devices that pass a self-test, and falls back to mode W elsewhere, so every device still activates.

Step 5

What the check after the build does

  • Every sealed output in the manifest is in the package, byte for byte, as the receipt recorded it. A stale or missing sealed file fails.
  • No plaintext input is in the package, as a whole file or as a fragment. Each input is sampled throughout, so a shipped piece of a few kilobytes is found; nested archives are opened.
  • Names inside the package are untrusted, and are reported without being interpreted.

It checks the assets you declared. It does not prove that nothing else valuable is in the package — that is what the scan and an assessment are for.

No account

The free scan

Before you integrate anything: does a release you already ship carry a model in plaintext? The scan fails the build if it does, and needs no account and no token.

- uses: rev3rse-dev/ipvault-scan-action@<commit-sha>
  with:
    path: app/build/outputs/apk/release/app-release.apk
ipvault-scan:
  stage: test
  image: alpine:3.20
  needs: [assemble-release]
  script:
    - apk add --no-cache curl
    - v=0.1.0; t=x86_64-unknown-linux-musl
    - curl -fsSLO "https://github.com/rev3rse-dev/ipvault-cli/releases/download/v$v/ipvault-$v-$t.tar.gz"
    - echo "6851005905e89080eda704445ec621f1a6feee1cc4d73d76701ad5b8ce9454a8  ipvault-$v-$t.tar.gz" | sha256sum -c -
    - tar -xzf "ipvault-$v-$t.tar.gz"
    - ./ipvault-$v-$t/ipvault scan app/build/outputs/apk/release/app-release.apk

A clean scan means no known plaintext model format was found within the scan’s limits. It does not mean the package is safe.

Troubleshooting

When something is refused

Every refusal names its reason. The common ones, as error / reason:

RefusalMeansDo
unauthorized / revoked, expiredThe CI token was revoked or has expired.Create a new token and update the secret.
policy / oidc_bindingNo binding admits this job. The message names what did not match: the ref, environment, workflow or repository.Publish from a ref the binding names, or change the binding.
policy / untrusted_eventA pull_request or pull_request_target job presented its OIDC token.Publish on push to a protected branch or tag, or from a release.
policy / unprotected_refA GitLab job on a ref that is not protected.Protect the branch or tag, or publish from one that is.
policy / app_scopeThe manifest names a different app from the one the credential publishes for.Use that app’s binding or token, or fix app.id.
policy / entitlement_scopeAn asset names an entitlement the credential may not add grants to.Widen the binding or token to that entitlement, or fix the manifest.
policy / key_ownerAnother app already publishes this key id.Set key_id on the asset.
policy / open_limitToo many releases for this app were begun and never finished.Abandon the stale ones: ipvault release abandon --release-id <id>.
conflict / operation_idThis attempt was begun from a different manifest or input.Restore the inputs, or publish as a new attempt (a re-run of the job is one).
(local) cannot be rebuiltA committed release’s sealed files were lost from the runner.Publish as a new attempt. Sealed files are never rebuilt under a key that is already registered.

The full interface is in the API reference.