Skip to content

postguard

GitHub · Rust · Core library and services

WARNING

This implementation has not been audited. Use at your own risk.

The main PostGuard repository. It contains the core encryption library, the Private Key Generator (PKG) server, WebAssembly bindings for browsers, a command-line client, and FFI bindings for native language integration.

The repo also includes a Docusaurus documentation site in the website/ directory, covering architecture, API reference, and Yivi integration. That content has been consolidated into this centralized docs site.

Workspace Structure

The repository is a Rust workspace with five crates:

CrateDescription
pg-coreCore library: metadata management, binary serialization, streaming encryption. Supports a native Rust backend (rust feature) and a WebCrypto-backed WASM backend (web + stream features).
pg-pkgHTTP API server (Actix-web) that runs a Private Key Generator instance
pg-wasmWebAssembly bindings via wasm-pack, used by the JavaScript SDK
pg-cliCommand-line tool for encrypting and decrypting files
pg-ffiFFI bindings for calling Rust code from other languages (used by postguard-dotnet)

The website/ directory contains a Docusaurus site deployed to GitHub Pages via the docs.yml workflow.

How It Works

PostGuard uses Identity-Based Encryption (IBE). Instead of public keys, the sender only needs the master public key and the recipient's identity (e.g. email address). To decrypt, the recipient proves their identity to the PKG via Yivi and receives a decryption key.

A typical session:

  1. The PKG generates a master key pair.
  2. The sender's client fetches the public master key from the PKG.
  3. The sender encrypts a message using the master public key and the recipient's identity.
  4. The ciphertext is sent to the recipient (through any channel).
  5. The recipient's client requests a decryption key from the PKG.
  6. The PKG starts a Yivi authentication session.
  7. The recipient proves their identity with the Yivi app.
  8. The PKG issues a decryption key for that identity.
  9. The recipient's client decrypts the message.

For the full protocol details, see the architecture overview and core concepts in the guide.

Cryptographic Primitives

PrimitiveImplementation
KEMCGW-KV anonymous IBE on BLS12-381 (ibe crate)
IBSGG identity-based signatures (ibs crate)
SymmetricAES-128-GCM (128-bit security to match BLS12-381)
HashingSHA3-512 for identity derivation

pg-core Feature Flags

pg-core supports two backends:

  • rust (default): uses RustCrypto crates for native Rust targets
  • web: uses the Web Crypto API for WASM in browsers

Streaming mode is enabled with the stream feature flag. When active, encryption and decryption process data in 256 KiB chunks instead of loading everything into memory.

Development

Prerequisites

  • Rust 1.90.0 or later
  • Docker and Docker Compose (for the local development environment with PostgreSQL and Yivi server)
  • wasm-pack (only for WASM development)
bash
# Install Rust
curl https://sh.rustup.rs -sSf | sh

# Install wasm-pack (if working on pg-wasm)
cargo install --git https://github.com/rustwasm/wasm-pack.git

Building

bash
# Build the full workspace
cargo build --release

# Build individual crates
cargo build --release -p pg-core
cargo build --release --bin pg-cli
cargo build --release --bin pg-pkg

Building WASM Bindings

bash
cd pg-wasm
wasm-pack build --release -d pkg/ --out-name index --scope e4a --target bundler

For web target (without a bundler):

bash
wasm-pack build --release -d pkg/ --out-name index --scope e4a --target web

Testing

bash
# Run all workspace tests
cargo test

# pg-core with all test features
cargo test -p pg-core --features test,rust,stream

# WASM tests (requires wasm-pack)
wasm-pack test --release --headless --chrome ./pg-wasm
wasm-pack test --release --headless --firefox ./pg-wasm

Development Environment

Docker Compose starts PostgreSQL and a Yivi (IRMA) server for local development:

bash
docker-compose up

Then run the PKG server against the local services:

bash
cargo run --release --bin pg-pkg server \
  -d postgres://devuser:devpassword@localhost/devdb \
  -t <irma_token> \
  -i http://localhost:8088

Environment Variables

VariableDescriptionDefault
IRMA_SERVERYivi/IRMA server URLhttps://is.yivi.app
DATABASE_URLPostgreSQL connection string
RUST_LOGLog level (debug, info, warn, error)

Running the PKG Server

Generate a master key pair first (run once):

bash
cargo run --release --bin pg-pkg gen

Then start the server:

bash
cargo run --release --bin pg-pkg server \
  -t <irma_server_token> \
  -i <irma_server_url> \
  -d <postgres_connection_string>

Or run via Docker:

bash
docker build -t postguard-pkg .
docker run -p 8080:8080 postguard-pkg server \
  -t <irma_token> \
  -i <irma_url> \
  -d <postgres_url>

Using the CLI

Encrypt a file:

bash
cargo run --bin pg-cli enc \
  -i '{"recipient@example.com": [{"t": "pbdf.sidn-pbdf.email.email", "v": "recipient@example.com"}]}' \
  --pub-sign-id '[{"t": "pbdf.gemeente.personalData.fullname"}]' \
  myfile.txt

This starts a Yivi session (displays a QR code) to obtain signing keys, then encrypts myfile.txt into myfile.txt.enc.

Decrypt a file:

bash
cargo run --bin pg-cli dec myfile.txt.enc

The CLI shows the recipient policies in the header, prompts you to select your identity, and starts a Yivi session to obtain your decryption key.

PKG Server API

The PKG server (pg-pkg) exposes an HTTP API. By default it listens on http://localhost:8080.

Public Parameters

MethodEndpointDescription
GET/v2/parametersFetch the Master Public Key (MPK). Supports ETag/Cache-Control caching.
GET/v2/sign/parametersFetch the public verification key for signature checking.

Yivi Sessions

MethodEndpointDescription
POST/v2/irma/startStart a Yivi identity verification session.
GET/v2/irma/jwt/{token}Retrieve the JWT result of a completed Yivi session.

Start a Yivi session with a request body like:

json
{
  "attr": {
    "recipient@example.com": {
      "t": 1234567890,
      "con": [
        {"t": "pbdf.sidn-pbdf.email.email", "v": "recipient@example.com"}
      ]
    }
  }
}

Key Issuance

MethodEndpointDescription
GET/v2/irma/key/{timestamp}Retrieve a User Secret Key (USK). Requires Authorization: Bearer <jwt>.
POST/v2/irma/sign/keyRetrieve signing keys. Authenticate with a Yivi JWT or API key (PG-API-<key>).

Request body for signing keys:

json
{
  "pubSignId": [
    {"t": "pbdf.gemeente.personalData.fullname"}
  ],
  "privSignId": [
    {"t": "pbdf.sidn-pbdf.email.email"}
  ]
}

Health

MethodEndpointDescription
GET/healthHealth check.
GET/metricsPrometheus metrics.

Authentication

The PKG supports two authentication methods:

  • JWT (Yivi Sessions): After a Yivi disclosure, the IRMA server issues a JWT. Pass it as Authorization: Bearer <jwt>.
  • API Keys: For server-to-server use, API keys bypass interactive Yivi sessions. Pass as Authorization: PG-API-<key>. Keys are stored in PostgreSQL with pre-configured attributes.

For the full API details with request/response examples, see the architecture page.

WASM Bindings (pg-wasm)

The @e4a/pg-wasm package provides WebAssembly bindings for PostGuard in browser environments. It supports both in-memory and streaming encryption/decryption using the Web Crypto API.

bash
npm install @e4a/pg-wasm

The package exports:

  • seal() and sealStream() for encryption (in-memory and streaming)
  • Unsealer and StreamUnsealer for decryption (in-memory and streaming)

Both streaming variants use the Web Streams API (ReadableStream/WritableStream). For usage examples and the full JavaScript/TypeScript API, see the SDK reference.

Encryption

In-memory:

typescript
import init, { seal } from '@e4a/pg-wasm';
await init();

const ciphertext = seal(masterPublicKey, {
  policy: {
    "recipient@example.com": {
      t: Math.floor(Date.now() / 1000),
      con: [{ t: "pbdf.sidn-pbdf.email.email", v: "recipient@example.com" }]
    }
  },
  pubSignKey: publicSigningKey,
  privSignKey: privateSigningKey,
}, plaintext);

Streaming:

typescript
import { sealStream } from '@e4a/pg-wasm';
await sealStream(masterPublicKey, sealOptions, readableStream, writableStream);

Decryption

In-memory:

typescript
import { Unsealer } from '@e4a/pg-wasm';
const unsealer = Unsealer.new(ciphertextBytes, verificationKey);
const recipients = unsealer.inspect_header();
const { plaintext, policy } = unsealer.unseal(recipientId, userSecretKey);

Streaming:

typescript
import { StreamUnsealer } from '@e4a/pg-wasm';
const unsealer = await StreamUnsealer.new(readableStream, verificationKey);
const recipients = unsealer.inspect_header();
const verifiedPolicy = await unsealer.unseal(recipientId, userSecretKey, writableStream);

Wire Format

PostGuard ciphertexts follow a binary wire format (V3):

PREAMBLE (10 bytes)
  PRELUDE      (4 bytes): 0x14 0x8A 0x8E 0xA7
  VERSION      (2 bytes): u16 big-endian (currently 0x0002)
  HEADER_LEN   (4 bytes): u32 big-endian

HEADER (variable)
  Header struct (bincode-serialized, max 1 MiB)
  SIG_LEN      (4 bytes): u32 big-endian
  HEADER_SIG   (variable): IBS signature over header

PAYLOAD (variable)
  AES-128-GCM encrypted data
    In-memory: single ciphertext + auth tag
    Streaming: 256 KiB segments, each with its own auth tag

Yivi Integration

PostGuard uses Yivi (formerly IRMA) for attribute-based identity verification. Identities are expressed as conjunctions of Yivi attributes:

json
[
  {"t": "pbdf.sidn-pbdf.email.email", "v": "alice@example.com"},
  {"t": "pbdf.gemeente.personalData.fullname", "v": "Alice Example"}
]

Each attribute has a type (t, a fully-qualified Yivi attribute identifier) and an optional value (v). Omitting the value checks only that the attribute exists.

Sender Flow (Signing Keys)

  1. Client calls POST /v2/irma/start with the sender's signing policy.
  2. PKG creates an IRMA disclosure request.
  3. Sender scans the QR code with the Yivi app.
  4. Client retrieves the session JWT via GET /v2/irma/jwt/{token}.
  5. Client requests signing keys via POST /v2/irma/sign/key with the JWT.
  6. PKG validates the JWT and issues signing keys.

Recipient Flow (Decryption Keys)

  1. Client reads the ciphertext header and identifies the recipient's hidden policy.
  2. Client calls POST /v2/irma/start with the required attributes.
  3. Recipient scans the QR code.
  4. Client retrieves the JWT via GET /v2/irma/jwt/{token}.
  5. Client requests the USK via GET /v2/irma/key/{timestamp}.
  6. PKG validates the JWT, derives the KEM identity, and issues the USK.

Hidden Policies

Ciphertext headers contain a hidden policy for each recipient. This shows attribute types but redacts values, so other recipients cannot see each other's identity details. Some attribute types may show partial hints (e.g., last 4 characters of a phone number) to help recipients identify which entry is theirs.

Releasing

This repository uses Release-plz for automated versioning and releases. When changes are merged to main, Release-plz creates a release PR. Merging that PR triggers:

  1. Crate publishing to crates.io (pg-core, pg-cli)
  2. GitHub releases with changelogs
  3. npm publishing of pg-wasm
  4. Multi-architecture Docker image for pg-pkg (pushed to GHCR)
  5. Platform-specific native libraries for pg-ffi (linux-x64, linux-arm64, osx-x64, osx-arm64, win-x64)

CI/CD

WorkflowTriggerWhat it does
build.ymlPush/PRFormatting checks, tests for all workspace members
delivery.ymlPush to mainRelease-plz, Docker build, FFI compilation, npm publish
docs.ymlPush to mainBuilds the Docusaurus site in website/ and deploys to GitHub Pages

Funding

Development of PostGuard was initially funded by the Next Generation Internet initiative (NGI0) and NLnet. The project is currently funded by a 4-year project from NWO under the name "Encryption 4 All".