π Web authentication based on Internet Computer identities.
IC-Auth connects identity-based signing in TypeScript and Rust with signature verification in Rust applications, IC canisters, and an HTTP service. It supports Ed25519, ECDSA P-256, ECDSA secp256k1, and IC canister signatures, including delegated session identities.
| Package | Purpose | Documentation |
|---|---|---|
ic_auth_types |
Shared delegation types, Base64URL byte wrappers, deterministic CBOR, XID identifiers and allocation | Rust types |
ic_auth_verifier |
Signature and delegation verification, HTTP envelopes, deep links, optional signing identities | Rust verifier |
ic_auth_verify_server |
JSON/CBOR HTTP verification service | HTTP API and deployment |
@ldclabs/ic-auth |
TypeScript identities, signing, compact wire types, and deterministic CBOR | TypeScript SDK |
ic-auth-worker |
Cloudflare Worker and Containers deployment of the Rust HTTP verifier | Cloudflare deployment |
Rust dependency examples use the workspace's 0.10 release line. The TypeScript package has its own version in package.json. To use changes from this checkout before publication, use Cargo path dependencies or build the TypeScript package locally.
- TypeScript client: sign a value or digest with
@ldclabs/ic-authand send the envelope to your backend. - Rust backend: enable
ic_auth_verifier/envelopeto verify envelopes; enableidentityto sign them too. - IC canister: use
ic_auth_typesandic_auth_verifierwithenvelope. Supply the IC time explicitly; leave the nativeidentityand upstreamxidfeatures disabled. - HTTP verification: run
ic_auth_verify_serverand callPOST /verifywith a JSON or CBOR body. - Cloudflare: deploy
ts/ic-auth-workerto run the same verifier in Containers, with regional routing and Worker service bindings.
[dependencies]
ic_auth_types = "0.10"
ic_auth_verifier = { version = "0.10", features = ["envelope"] }npm install @ldclabs/ic-auth @icp-sdk/core @noble/hashes cborgThe SDK is an ES module package. It supports browser applications and declares Node.js 20+ support.
import {
Ed25519KeyIdentity,
bytesToBase64Url,
deterministicEncode,
signMessage,
toDelegationIdentity
} from '@ldclabs/ic-auth'
const identity = toDelegationIdentity(Ed25519KeyIdentity.generate())
const message = { challenge: 'login-123', origin: 'https://example.com' }
const envelope = await signMessage(identity, message)
const token = bytesToBase64Url(deterministicEncode(envelope))
const authorization = `ICP ${token}`Send authorization to an application endpoint that uses the Rust envelope parser. The standalone verification service instead expects the token in its signed_envelope request-body field; see its complete client example.
This example requires ic_auth_verifier with envelope, plus candid = "0.10" and http = "1" as direct dependencies.
use candid::Principal;
use http::HeaderMap;
use ic_auth_verifier::SignedEnvelope;
fn authenticate(
headers: &HeaderMap,
now_ms: u64,
expected_digest: &[u8],
target: Option<Principal>,
) -> Result<Principal, String> {
let envelope = SignedEnvelope::from_authorization(headers)
.ok_or_else(|| "missing or malformed IC-Auth envelope".to_string())?;
envelope.verify(now_ms, target, Some(expected_digest))?;
Ok(envelope.sender())
}Compute expected_digest from the request or challenge your application expects. sender() derives a principal from the public key; use it as an authenticated identity only after successful verification.
An envelope contains the original public key (p), signature (s), optional digest (h), and optional delegation chain (d). Delegations authorize successive session keys while the principal remains derived from the original public key.
| Operation | Bytes signed |
|---|---|
TypeScript signMessage(identity, value) |
SHA3-256 of deterministicEncode(value) |
Rust SignedEnvelope::sign_message(identity, bytes) |
SHA3-256 of the supplied bytes |
TypeScript signArbitrary / Rust SignedEnvelope::sign_digest |
The supplied bytes directly |
For matching signatures across languages, encode the same value as deterministic CBOR in both languages before hashing. Use byte strings (Uint8Array in TypeScript and a byte wrapper in Rust) for binary data. A numeric array and a byte string have different CBOR encodings.
Rust JSON byte wrappers emit padded Base64URL with a b64: prefix and accept unprefixed input too. HTTP envelope tokens use unprefixed Base64URL. CBOR stores binary fields as byte strings. See the type and wire reference for compact keys and Candid compatibility.
Verification checks signatures, delegation expiration, an optional target, and an optional expected digest. Applications define the signed request schema and enforce challenge expiry, one-time use, and authorization. The verifier does not maintain a replay ledger or infer an HTTP method, path, or body from the envelope.
From the repository root:
cargo run -p ic_auth_verify_server
curl --fail http://127.0.0.1:8080/The default listen address is 127.0.0.1:8080; set SOCKET_ADDR to override it. See the server README for Docker, content types, status codes, and verification requests.
Use a current stable Rust toolchain supporting edition 2024. Run Rust checks from the repository root:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features
cargo test --workspace --all-featuresCheck the canister-facing configurations separately:
rustup target add wasm32-unknown-unknown
cargo check -p ic_auth_types --target wasm32-unknown-unknown
cargo check -p ic_auth_verifier --features envelope --target wasm32-unknown-unknownFor the TypeScript package, use Node.js and pnpm:
cd ts/ic-auth
pnpm install --frozen-lockfile
pnpm build
pnpm test
pnpm coverageBoth TypeScript packages belong to the root pnpm workspace and share pnpm-lock.yaml. From ts/ic-auth-worker:
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm buildpnpm build checks the Worker bundle without deploying. Local container development additionally requires Docker; see the Worker README.
Copyright Β© 2024-2026 LDC Labs.
Licensed under the MIT License. See LICENSE.