Token primitives

Holder-side cryptographic primitives — Credential, Token, KeyPair, etc. Lives on the @owlid/sdk/native subpath. Implemented in Rust, compiled to native (Node) or WASM (browser) automatically. Importing this subpath is what loads the WASM module; if you only call REST endpoints, stay on the @owlid/sdk root and skip it.

See bundler & WASM setup for the Vite / Webpack / Next.js configuration the native subpath needs.

import { Credential, Token, KeyPair, PublicKey } from '@owlid/sdk/native'

These run entirely on the holder's device. Token generation never round-trips to the platform.

Classes

KeyPair

Ed25519 keypair for the holder.

const kp = KeyPair.generate()
const restored = KeyPair.fromHex(privateHex)

kp.publicKey() // → PublicKey
kp.toHex() // 64 hex chars (private seed)
kp.sign(messageBytes) // → Signature

PublicKey

Ed25519 public key.

const pk = PublicKey.fromHex(hex)
pk.toHex() // 64 hex chars
pk.verify(messageBytes, sig) // boolean

Document

Unsigned attribute container. Held by the issuer before signing.

const doc = Document.fromJson(jsonString)
const credential = doc.issue(issuerKeyPair)

Credential

Signed credential — Merkle root + issuer signature + attributes. The output of issuance, what holders store.

const credential = Credential.fromJson(storedJson)

credential.rootHash() // 64 hex chars (Merkle root)
credential.toJson() // serialise

// One-phase signing (raw Ed25519 owner)
credential.prove(request, ownerKeyPair, ttlSeconds) // → Token

// Two-phase (WebAuthn / ring signature)
credential.prepare(request, ttlSeconds) // → PreparedToken

Token

Verifiable proof token.

token.toCompact() // "OID1:..." — fits a QR code
Token.fromCompact(compact) // restore

token.subjects() // disclosed attributes (JSON string)
token.challenge() // bound challenge
token.rootHash() // credential root hash
token.ttl() // seconds
token.activationTime() // unix epoch seconds
token.zkProofCount() // number of ZK predicate proofs

// Holder-side verification (no platform round-trip)
token.verify([trustedIssuerPubKey], expectedChallenge, /* revokedRoots */ [])

// WebAuthn finalisation
Token.finalizeWebauthn(prepared, webauthnSig, credentialPublicKeyHex)

// Ring-signature finalisation (anonymous owner authentication)
Token.finalizeRingSig(prepared, privateKeyHex, ringPublicKeysHex)

PreparedToken

Two-phase signing intermediate.

prepared.challenge() // base64url, pass to navigator.credentials.get()
prepared.payloadJson() // unsigned payload (debugging)
prepared.toJson() // serialise

Proof requests

interface ProofRequest {
  /** Attributes to reveal as plaintext. */
  disclose: string[]
  /** Zero-knowledge predicates to attach. */
  predicates: PredicateRequest[]
  /** Hex public keys you trust to have issued the credential. */
  trustedIssuers: string[]
  /** Random challenge from the verifier. */
  challenge: string
}

interface PredicateRequest {
  attribute: string
  /** 'GreaterOrEqual' (e.g. age ≥ 18) or 'InSet' (e.g. nationality ∈ EU). */
  op: 'GreaterOrEqual' | 'InSet'
  /**
   * Wire value, JSON-encoded.
   *  - `GreaterOrEqual`: a number (e.g. `'18'`).
   *  - `InSet`: a registered dataset name string (e.g. `'"eu"'`).
   *
   * For `InSet`, the holder ships only the dataset name — the verifier
   * recomputes the canonical Merkle root from that name and pins the proof's
   * public input against it.
   */
  value: string
}

Hash helpers

import { blake3, sha256 } from '@owlid/sdk/native'

blake3(buffer) // 64 hex chars
sha256(buffer) // 64 hex chars

Compact format

Tokens encode through JSON → CBOR → zstd → Base45 → "OID1:" prefix. Typical size 500–1500 bytes — fits a QR code (Version 25–40 at error correction Q).

Groth16 proving keys

ZK predicates are proved with Groth16 over BLS12-381. There are three circuits:

CircuitUsed for
age_rangedateOfBirth GreaterOrEqual (e.g. age ≥ 18)
kyc_statusverificationLevel / isResident GreaterOrEqual
nationalitynationality InSet (Pedersen Merkle membership)

Each circuit needs a proving key (~150 KB–350 KB). Two delivery shapes:

  • Native (Node) builds: keys are baked into the addon. No setup needed.
  • Browser/WASM builds: keys are not bundled (would inflate the WASM blob). The SDK fetches them lazily on first proof from the verification service, caches in IndexedDB, and reuses across sessions.

Holder helpers (signToken, signTokenWithPasskey, respondToPresentation) load the right keys automatically — apps don't need to call anything. The default fetch URL is ${getVerificationUrl()}/zk-keys/<circuit>.pk.bin — same trusted origin you already verify against, with Cache-Control: public, immutable.

Override key delivery

Apps that want to bundle their own keys, host them on a CDN, or use a custom transport call configureProvingKeys once at startup:

import { configureProvingKeys } from '@owlid/sdk/native'

// Bundle bytes (e.g. via Vite ?url import)
import ageRangeUrl from './zk/age_range.pk.bin?url'
configureProvingKeys({
  bytes: { age_range: new Uint8Array(await (await fetch(ageRangeUrl)).arrayBuffer()) },
})

// Fetch from custom origin
configureProvingKeys({ baseUrl: 'https://cdn.mine.com/zk' })

// Fully custom loader
configureProvingKeys({
  loader: async (circuit) => {
    const r = await fetch(`/my/keys/${circuit}.bin`)
    return new Uint8Array(await r.arrayBuffer())
  },
})

// Disable IndexedDB cache (e.g. tests)
configureProvingKeys({ cache: false })

Sources resolve in priority order: bytes ▶ in-memory cache ▶ IndexedDB ▶ loader (or default fetch). The native build always reports provingKeysRequired() === false and skips the loader entirely.

Eager prefetch

To warm the cache at app startup (e.g. during the splash screen) so the first proof has zero latency:

import { ensureProvingKeys, ensureProvingKeysFor } from '@owlid/sdk/native'

await ensureProvingKeys() // load all three (~660 KB total)
// or
await ensureProvingKeysFor(['age_range']) // just the one

Both are no-ops on native and idempotent on WASM.

Trusted setup

Proving keys ship with the OwlID platform and are fetched once per circuit from ${getVerificationUrl()}/zk-keys/<circuit>.pk.bin. The URL and the binary format never change — only the bytes do when OwlID rotates the keys.