qnsp 0.2.0

Official Rust SDK for the QNSP Quantum-Native Security Platform — post-quantum cryptography (ML-KEM, ML-DSA, SLH-DSA, Falcon via liboqs), PQC-encrypted vault, KMS, and immutable audit trails.
Documentation
# qnsp — Rust SDK for the Quantum-Native Security Platform

[![Crates.io](https://img.shields.io/crates/v/qnsp.svg)](https://crates.io/crates/qnsp)
[![docs.rs](https://docs.rs/qnsp/badge.svg)](https://docs.rs/qnsp)
[![License](https://img.shields.io/crates/l/qnsp.svg)](./LICENSE)

Typed async Rust client for QNSP — post-quantum cryptography (ML-KEM, ML-DSA, SLH-DSA, Falcon via liboqs), PQC-encrypted vault, server-side KMS, immutable audit trails. Same wire contracts as the official `@qnsp/*` TypeScript SDKs, the `qnsp` Python SDK, and the `github.com/cuilabs/qnsp-public/sdks/go/qnsp` Go SDK — pick whichever language fits your stack and the byte-for-byte outputs round-trip.

> **Free tier available.** Free-forever account at <https://cloud.qnsp.cuilabs.io/auth> — 60-second signup, no credit card. Includes 10 GB PQC storage, 50 000 API calls/month, 20 KMS keys, 25 vault secrets.

## Install

Base install (HTTP clients for vault, KMS, audit — no native deps):

```bash
cargo add qnsp
```

With local PQC primitives (`qnsp::crypto` — wraps the [`oqs`](https://crates.io/crates/oqs) crate 0.11):

```bash
cargo add qnsp --features crypto
```

The `crypto` feature pulls in [`oqs`](https://crates.io/crates/oqs) which delegates to [`oqs-sys`](https://crates.io/crates/oqs-sys); `oqs-sys` builds [`liboqs`](https://github.com/open-quantum-safe/liboqs) from source via `cmake`. You'll need a C toolchain and `cmake` available at build time. macOS / Linux build out of the box; on Windows you'll want the MSVC toolchain plus a `cmake` install.

Tested on Rust 1.75+. The crate is `tokio`-based on the async side.

## Quick start

```rust
use qnsp::{Client, ClientOptions};
use qnsp::vault::CreateSecretRequest;
use qnsp::kms::CreateKeyRequest;
use qnsp::audit::LogEventRequest;
use base64::{engine::general_purpose::STANDARD, Engine};

#[tokio::main]
async fn main() -> Result<(), qnsp::Error> {
    let c = Client::new(ClientOptions::with_api_key(std::env::var("QNSP_API_KEY").unwrap()))?;

    // ── Vault — PQC-encrypted secret storage ─────────────────────────
    let secret = c.vault().create_secret(CreateSecretRequest {
        name: "openai-api-key".into(),
        payload_b64: STANDARD.encode(b"sk-..."),
        algorithm: Some("ml-kem-768".into()),
        metadata: None,
    }, None).await?;

    // ── KMS — server-side PQC keys ──────────────────────────────────
    let key = c.kms().create_key(CreateKeyRequest {
        algorithm: "ml-dsa-65".into(),
        purpose: "signing".into(),
        metadata: None,
    }, None).await?;
    let key_id = key["keyId"].as_str().unwrap();
    let signature = c.kms().sign(key_id, b"hello", None).await?;
    assert!(c.kms().verify(key_id, b"hello", &signature).await?);

    // ── Audit — immutable, hash-chained event log ───────────────────
    c.audit().log_event(LogEventRequest {
        event_type: "model.inference".into(),
        payload: serde_json::Map::from_iter([
            ("modelId".to_string(), serde_json::Value::String("gpt-4o".into())),
            ("latencyMs".to_string(), serde_json::Value::from(412)),
        ]),
        tags: None,
    }, None).await?;

    Ok(())
}
```

## Local PQC primitives

`qnsp::crypto` wraps the `oqs` crate so you don't have to write `oqs::kem` / `oqs::sig` calls directly, and so the algorithm-name surface matches the rest of the QNSP ecosystem (TypeScript, Python, Go):

```rust
#[cfg(feature = "crypto")]
{
    use qnsp::crypto::{kem_round_trip, sig_round_trip};

    let (pk, sk, ct, ss) = kem_round_trip("ML-KEM-768")?;
    assert_eq!(ss.len(), 32);

    let (sig_pk, sig_sk, signature) = sig_round_trip("ML-DSA-65", b"hello")?;
}
```

For full lifecycle control (generate once, sign or decapsulate many times), call `oqs::kem::Kem::new(qnsp::crypto::kem_algorithm("ML-KEM-768")?)` directly — the resolver functions are public.

## Verifying inbound webhooks

QNSP signs every webhook with HMAC-SHA-256. Verify the **raw body** before parsing JSON:

```rust
use axum::{extract::Request, http::StatusCode, response::IntoResponse};
use qnsp::{parse_webhook, MAX_WEBHOOK_SKEW};

async fn handle(req: Request) -> Result<impl IntoResponse, (StatusCode, String)> {
    let sig = req.headers().get("x-qnsp-signature")
        .and_then(|h| h.to_str().ok()).unwrap_or("").to_string();
    let ts  = req.headers().get("x-qnsp-timestamp")
        .and_then(|h| h.to_str().ok()).map(String::from);
    let body = axum::body::to_bytes(req.into_body(), 1_000_000).await
        .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;

    let event = parse_webhook(
        &body,
        &sig,
        ts.as_deref(),
        &std::env::var("QNSP_WEBHOOK_SECRET").unwrap(),
        MAX_WEBHOOK_SKEW,
        None,
    ).map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;

    if event.event_type == "key.rotated" {
        // ...
    }
    Ok(StatusCode::OK)
}
```

The verifier runs HMAC comparison in constant time, rejects timestamps older than 5 minutes by default (replay protection), and refuses payloads missing required fields.

## Error handling

All errors flow through `qnsp::Error`:

| Variant | When |
| --- | --- |
| `qnsp::Error::Network(_)` | DNS, TLS, timeout, or connection failure |
| `qnsp::Error::Auth(_)` | API key rejected at activation |
| `qnsp::Error::Api(_)` | A service returned 4xx/5xx with a structured body |
| `qnsp::Error::Webhook(_)` | HMAC mismatch, expired timestamp, malformed body, etc. |

```rust
match c.vault().get_secret("missing").await {
    Err(qnsp::Error::Api(e)) if e.status_code == 404 => {
        println!("not found");
    }
    Err(e) => return Err(e),
    Ok(secret) => println!("got {secret:?}"),
}
```

## Activation + tier introspection

`qnsp::Client` performs a one-shot handshake against `/billing/v1/sdk/activate` on first use. The result is cached in memory; subsequent calls reuse it until ~1 minute before expiry. You can inspect the current activation:

```rust
let tenant_id = c.tenant_id().await?;
let tier      = c.tier().await?;
let limits    = c.limits().await?;
let sse_on    = c.has_feature("sseEnabled").await?;
```

If the activation token is rotated server-side, the SDK invalidates its cache and retries the originating request once on a 401.

## What's covered today

Customer-facing modules (every QNSP service callable through the edge gateway today):

- `qnsp::vault``create_secret`, `get_secret`, `get_secret_version`, `rotate_secret`, `delete_secret`, `list_secret_versions` — wraps `apps/vault-service`
- `qnsp::kms``create_key`, `list_keys`, `get_key`, `rotate_key`, `delete_key`, `sign`, `verify`, `wrap`, `unwrap_` — wraps `apps/kms-service`
- `qnsp::audit``log_event`, `ingest_events` (batch), `list_events` — wraps `apps/audit-service`
- `qnsp::auth``login`, `refresh_token`, `revoke`, WebAuthn passkey lifecycle, `mfa_challenge`/`mfa_verify`, `federate_saml`/`federate_oidc`, `evaluate_risk` — wraps `apps/auth-service`
- `qnsp::tenant``create_tenant`, `get_tenant`, `update_tenant`, `list_tenants`, `get_crypto_policy`, `upsert_crypto_policy`, `get_current_health`, `get_current_quotas` — wraps `apps/tenant-service`
- `qnsp::access``create_role`, `get_role`, `list_roles`, `delete_role`, `assign_role`, `revoke_role_assignment`, `check_permission` — wraps `apps/access-control-service`
- `qnsp::billing``get_entitlements`, `ingest_meter`, `ingest_meters`, `list_invoices`, `get_invoice`, `get_credit_balance` — wraps `apps/billing-service`
- `qnsp::crypto_inventory``list_assets`, `get_asset`, `get_asset_stats`, `discover_assets`, `get_readiness_score` — wraps `apps/crypto-inventory-service` (CBOM)
- `qnsp::storage``put_object`, `get_object`, `delete_object`, `list_objects`, `list_buckets` — wraps `apps/storage-service` (SSE-X)
- `qnsp::search``create_index`, `list_indexes`, `delete_index`, `upsert_vectors`, `query` — wraps `apps/search-service` (vector search)
- `qnsp::ai``register_model`, model lifecycle, `submit_workload`, `invoke_inference`, `register_artifact` — wraps `apps/ai-orchestrator`

Local primitives + integration:

- `qnsp::crypto` (feature-gated on `crypto`) — ML-KEM (512/768/1024), ML-DSA (44/65/87), SLH-DSA (12 variants), Falcon (512/1024), plus HQC, BIKE, FrodoKEM, Classic-McEliece, MAYO, CROSS — every FIPS 203/204/205 finalist exposed by `oqs` 0.11
- `qnsp::parse_webhook` / `qnsp::verify_webhook_signature` — HMAC-SHA-256 signature verification + typed `qnsp::WebhookEvent`
- `qnsp::Client::new` — API-key activation against `/billing/v1/sdk/activate` with caching and 401 retry

## What's coming

- Async streaming for `Storage::get_object` on large objects
- Pre-built `qnsp::test_support` helper that mocks the QNSP API for tests
- Generated typed responses (currently `serde_json::Value`) for every method

## License

Apache-2.0. See [LICENSE](../../../LICENSE).