npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

verifiable-credential-toolkit

v0.7.0

Published

Provides methods for handling, constructing and signing Verifiable Credentials

Readme

Verifiable Credential Toolkit

A Rust library (with WASM/JavaScript bindings) for creating, signing, and verifying W3C Verifiable Credentials. It signs with Ed25519 or post-quantum ML-DSA (FIPS 204) to ensure credentials are tamper-proof and can be independently verified.

Table of Contents


What Are Verifiable Credentials?

Verifiable Credentials (VCs) are a W3C standard for expressing credentials (e.g. identity documents, certificates, attestations about devices or people) in a way that is:

  • Tamper-evident — any modification to the credential after signing is detectable.
  • Machine-readable — credentials are structured JSON that software can parse and validate.
  • Decentralised — no central authority is needed to verify a credential; anyone with the issuer's public key can check it.

A typical VC workflow looks like this:

┌──────────┐         ┌──────────┐         ┌──────────┐
│  Issuer  │         │  Holder  │         │ Verifier │
│          │         │          │         │          │
│ Creates  │ Signed  │ Stores & │ Presents│ Checks   │
│ & signs  ├────────►│ carries  ├────────►│ signature│
│ the VC   │   VC    │ the VC   │   VC    │ & data   │
└──────────┘         └──────────┘         └──────────┘

An issuer signs a credential with their private key; a holder stores and presents it; a verifier checks the signature with the issuer's public key. This toolkit handles the issuer and verifier ends — signing and verifying.


Quick Start

Rust

use verifiable_credential_toolkit::{
    Algorithm, UnsignedVerifiableCredential, generate_keypair,
};

// 1. Generate a keypair (signing_key: SigningKey, verifying_key: VerifyingKey).
//    The algorithm travels with each key; pass ML-DSA-44/65/87 here for post-quantum.
let keypair = generate_keypair(Algorithm::Ed25519);

// 2. Define a credential as JSON
let vc_json = r#"{
    "@context": ["https://www.w3.org/ns/credentials/v2"],
    "type": ["VerifiableCredential"],
    "issuer": "https://example.com/issuers/sensor-manufacturer",
    "credentialSubject": {
        "id": "urn:uuid:device-001",
        "name": "Temperature Sensor A"
    }
}"#;

// 3. Parse and sign
let unsigned_vc: UnsignedVerifiableCredential =
    serde_json::from_str(vc_json).expect("Invalid VC JSON");
let signed_vc = unsigned_vc.sign(&keypair.signing_key).expect("Signing failed");

// 4. Verify
signed_vc.verify(&keypair.verifying_key).expect("Verification failed");
println!("Credential verified successfully!");

JavaScript (Node.js)

import {
  sign,
  verify,
  generate_keypair,
} from "./pkg/verifiable_credential_toolkit.js";

// 1. Generate keys
const keypair = generate_keypair();

// 2. Create an unsigned credential
const unsignedVC = {
  "@context": ["https://www.w3.org/ns/credentials/v2"],
  type: ["VerifiableCredential"],
  issuer: "https://example.com/issuers/sensor-manufacturer",
  credentialSubject: {
    id: "urn:uuid:device-001",
    name: "Temperature Sensor A",
  },
};

// 3. Sign and verify
const signedVC = sign(unsignedVC, keypair.signing_key());
const isValid = verify(signedVC, keypair.verifying_key());
console.log("Valid:", isValid); // true

Installation

As a Rust dependency

Add to your Cargo.toml:

[dependencies]
verifiable-credential-toolkit = "0.7"

Or install directly from GitHub:

[dependencies]
verifiable-credential-toolkit = { git = "https://github.com/nqminds/verifiable-credential-toolkit" }

The protobuf Rust bindings are generated automatically at build time from src/proto_schemas/vc.proto into Cargo's build output directory, so the generated .rs files do not need to be committed. protoc is supplied by the vendored build dependency used in build.rs.

As a WASM/JavaScript package

You need the wasm32-unknown-unknown Rust target and the wasm-bindgen-cli tool:

Currently the bindgen format is unstable enough that these two schema versions must exactly match. You can accomplish this by either updating this binary or the wasm-bindgen dependency in the Rust project.

You can install a particular the binary with: cargo install -f wasm-bindgen-cli --version <your version number here>

# Install the WASM compilation target (one-time setup)
rustup target add wasm32-unknown-unknown

# Install wasm-bindgen-cli (version must match the wasm-bindgen dependency in Cargo.toml)
cargo install wasm-bindgen-cli --version 0.2.100

Then build:

# Compile to WASM
cargo build --target wasm32-unknown-unknown --release

# Generate JS/TS bindings for browser usage
wasm-bindgen --target web --out-dir pkg \
  target/wasm32-unknown-unknown/release/verifiable_credential_toolkit.wasm

# Or for Node.js usage
wasm-bindgen --target nodejs --out-dir pkg \
  target/wasm32-unknown-unknown/release/verifiable_credential_toolkit.wasm

This generates a pkg/ directory containing the compiled WASM module and JavaScript/TypeScript bindings that you can import directly.

CLI tools

# Install both CLI tools
cargo install --path .

# Or run directly
cargo run --bin generate_keys
cargo run --bin vc_signer

Core Concepts

Credential Structure

A Verifiable Credential is a JSON object with this structure:

{
  // Required: JSON-LD context URLs defining the vocabulary
  "@context": ["https://www.w3.org/ns/credentials/v2"],

  // Optional: unique identifier for this credential
  "id": "urn:uuid:9a3e3c0e-2db0-412a-95c7-cf5520ba78df",

  // Required: credential type(s) — must include "VerifiableCredential"
  "type": ["VerifiableCredential", "DeviceCertificate"],

  // Required: who issued this credential (URL or object with "id")
  "issuer": "https://example.com/issuers/device-manufacturer",

  // Required: the actual claims being made
  "credentialSubject": {
    "id": "urn:uuid:device-001",
    "name": "Temperature Sensor A",
    "model": "TS-3000",
  },

  // Optional: when this credential becomes valid (ISO 8601)
  "validFrom": "2024-01-01T00:00:00Z",

  // Optional: when this credential expires (ISO 8601)
  "validUntil": "2030-01-01T00:00:00Z",

  // Optional: JSON Schema reference for validating credentialSubject
  "credentialSchema": {
    "id": "https://example.com/schemas/device.json",
    "type": "JsonSchema",
  },

  // Added by signing — the cryptographic proof
  "proof": {
    "type": "DataIntegrityProof",
    "cryptosuite": "eddsa-jcs-2022",
    "proofPurpose": "assertionMethod",
    "proofValue": "z-multibase-encoded-signature...",
  },
}

Signature algorithms

The toolkit signs and verifies with these algorithms, selected via the [Algorithm] enum:

| Algorithm | Algorithm variant | Private key | Public key | cryptosuite | |---|---|---:|---:|---| | Ed25519 (EdDSA) | Ed25519 | 32 | 32 | eddsa-jcs-2022 | | ML-DSA-44 (FIPS 204, cat. 2) | MlDsa44 | 2560 | 1312 | mldsa44-jcs-2025 | | ML-DSA-65 (FIPS 204, cat. 3) | MlDsa65 | 4032 | 1952 | mldsa65-jcs-2025 | | ML-DSA-87 (FIPS 204, cat. 5) | MlDsa87 | 4896 | 2592 | mldsa87-jcs-2025 |

ML-DSA (the NIST post-quantum standard) comes from the ml-dsa crate and is just another Algorithm — no separate API. Typed keys carry their algorithm and use the same sign / verify as Ed25519; a raw-byte path (generate_keypair_bytes, sign_with_algorithm, verify_auto) covers HSM / wasm / cross-language interop. See Usage for both.

⚠️ Provisional ML-DSA cryptosuite identifiers. The W3C vc-di-mldsa cryptosuite is still a Working Draft with no finalized identifier, multikey codec, or canonicalization choice. The mldsa{44,65,87}-jcs-2025 strings above are a bilateral convention for closed deployments (e.g. NATO IC 2026 partners) — signing and verifying parties must agree on them out of band and document them; they are not interoperable with arbitrary third-party verifiers until the standard finalizes.

Canonical signing

Signatures are computed over the credential in JCS canonical form (RFC 8785) — keys sorted, numbers normalized — so a signature survives a serialize/deserialize round-trip and is identical across JSON, CBOR, and Protobuf (the proof is over the canonical form, not the wire bytes). The proof is a DataIntegrityProof with a multibase (base58btc) proofValue. ML-DSA signing is hedged (non-deterministic); Ed25519 is deterministic.

Compatibility note: signatures are not interchangeable across encodings of the proofValue. 0.5.x signed over non-canonical JSON; 0.6.x used a base64 proofValue; 0.7.0 uses a multibase proofValue. Credentials issued by earlier versions must be re-signed. 0.7.0 is also breaking in that verify now rejects an unknown or missing cryptosuite, and VcError::SignatureVerificationFailed is a unit variant.


Usage

Rust Library

Generate a Keypair

use verifiable_credential_toolkit::{generate_keypair, Algorithm};

// Ed25519 (classical). The algorithm travels with each key.
let keypair = generate_keypair(Algorithm::Ed25519);
// Post-quantum instead — identical API, just a different Algorithm:
// let keypair = generate_keypair(Algorithm::MlDsa44);
// let keypair = generate_keypair(Algorithm::MlDsa65);
// let keypair = generate_keypair(Algorithm::MlDsa87);

// keypair.signing_key:   SigningKey   — keep secret
// keypair.verifying_key: VerifyingKey — distribute to verifiers
// Distinct types (a public key can't be passed where a private key is expected),
// each carrying its Algorithm, so the same sign/verify works for every algorithm.

// Save the raw key bytes to files.
std::fs::write("issuer.priv", keypair.signing_key.as_bytes()).unwrap();
std::fs::write("issuer.pub", keypair.verifying_key.as_bytes()).unwrap();

// Prefer raw bytes (HSM, wasm, or cross-language interop)? Skip the typed keys:
// let (private_key, public_key) = generate_keypair_bytes(Algorithm::Ed25519);

Construct a Credential

You can deserialize one from JSON, or build it programmatically with the fluent builder (which mirrors the Proof builder — required fields up front, optional setters chained, then .build()):

use verifiable_credential_toolkit::{Issuer, LanguageValue, UnsignedVerifiableCredential};
use url::Url;
use serde_json::json;

// Option A — build it programmatically with the fluent builder.
let unsigned_vc = UnsignedVerifiableCredential::builder(
    vec![Url::parse("https://www.w3.org/ns/credentials/v2").unwrap()],
    vec!["VerifiableCredential".to_string()],
    Issuer::Url(Url::parse("https://example.com/issuer").unwrap()),
    // Or an issuer object carrying extra properties:
    // Issuer::Object(IssuerObject { id: Url::parse("https://example.com/issuer").unwrap(),
    //     additional_properties: Some(std::collections::HashMap::from([("name".into(), json!("Acme"))])) }),
    json!({ "id": "urn:uuid:device-1", "name": "Sensor A" }),
)
.id(Url::parse("urn:uuid:9a3e3c0e-2db0-412a-95c7-cf5520ba78df").unwrap())
// Every optional field is a chainable setter before .build():
// .name(LanguageValue::PlainString("Device Certificate".to_string()))
// .description(LanguageValue::PlainString("Manufacturer-issued".to_string()))
// .valid_from(chrono::Utc::now())
// .valid_until(chrono::Utc::now() + chrono::Duration::days(365))
// .credential_status(status)
// .credential_schema(vec![credential_schema])
.build();

// Option B — deserialize one from JSON (a file, an API response, …):
// let unsigned_vc: UnsignedVerifiableCredential = serde_json::from_str(vc_json)?;

Sign a Credential

use verifiable_credential_toolkit::{Algorithm, SigningKey, UnsignedVerifiableCredential};

// Load credential from JSON (from a file, API response, etc.)
let vc_json = std::fs::read_to_string("credential.json").unwrap();
let unsigned_vc: UnsignedVerifiableCredential =
    serde_json::from_str(&vc_json).expect("Invalid VC JSON");

// Load a private key (length-checked against the algorithm). Pass the Algorithm
// that matches the key you loaded — e.g. Algorithm::MlDsa65 for an ML-DSA-65 key.
let signing_key = SigningKey::new(Algorithm::Ed25519, &std::fs::read("issuer.priv").unwrap())
    .expect("Invalid private key");

// Sign — produces a VerifiableCredential with a DataIntegrityProof attached.
let signed_vc = unsigned_vc.sign(&signing_key).expect("Signing failed");

// Raw-bytes alternative (no typed key) — pass the algorithm and key bytes directly:
// let signed_vc = unsigned_vc.sign_with_algorithm(Algorithm::Ed25519, &private_key_bytes)?;

// Serialise and save
let output = serde_json::to_string_pretty(&signed_vc).unwrap();
std::fs::write("credential_signed.json", output).unwrap();

Verify a Credential

use verifiable_credential_toolkit::{Algorithm, VerifiableCredential, VerifyingKey};

// Load the signed credential
let vc_json = std::fs::read_to_string("credential_signed.json").unwrap();
let signed_vc: VerifiableCredential =
    serde_json::from_str(&vc_json).expect("Invalid signed VC");

// Load the issuer's public key (validates the length matches the algorithm)
let verifying_key = VerifyingKey::new(Algorithm::Ed25519, &std::fs::read("issuer.pub").unwrap())
    .expect("Invalid public key");

// Verify — checks the signature, the validity period (validFrom/validUntil), and
// that the proof's cryptosuite matches the key's algorithm.
match signed_vc.verify(&verifying_key) {
    Ok(()) => println!("Credential is valid and untampered"),
    Err(e) => eprintln!("Verification failed: {}", e),
}

// Raw-bytes alternatives (public key as &[u8]):
// signed_vc.verify_auto(&public_key_bytes)?;                              // reads the algorithm from proof.cryptosuite
// signed_vc.verify_with_algorithm(Algorithm::Ed25519, &public_key_bytes)?; // caller asserts the algorithm

Sign & Verify in CBOR or Protobuf

Every algorithm works across JSON, CBOR, and Protobuf. Each wire format is a CredentialCodec; because the signature is over the format-independent JCS canonical form, a credential signed in one format verifies in any other.

use verifiable_credential_toolkit::{
    Algorithm,
    bindings::{cbor::Cbor, protobuf::Protobuf, CredentialCodec},
};

// Encode an unsigned credential to CBOR, sign it, and verify — all on bytes.
let unsigned_bytes = Cbor::encode_unsigned(&unsigned_vc)?;
let signed_bytes = Cbor::sign(&unsigned_bytes, Algorithm::Ed25519, &private_key)?;
Cbor::verify_auto(&signed_bytes, &public_key)?;   // reads the algorithm from the proof
// Assert the algorithm explicitly instead:
// Cbor::verify(&signed_bytes, Algorithm::Ed25519, &public_key)?;

// Protobuf is the same trait — swap Cbor for Protobuf (and any algorithm):
// let signed_pb = Protobuf::sign(&Protobuf::encode_unsigned(&unsigned_vc)?, Algorithm::MlDsa65, &private_key)?;
// Protobuf::verify_auto(&signed_pb, &public_key)?;

// Cross-format: re-encode a signed credential and it still verifies.
// let as_protobuf = Protobuf::encode_signed(&Cbor::decode_signed(&signed_bytes)?)?;
// Protobuf::verify_auto(&as_protobuf, &public_key)?;

Sign externally (HSM / out-of-process)

Get the exact canonical bytes with signing_payload, sign them wherever the key lives, then wrap the raw signature in a proof — nothing else touches the key.

use verifiable_credential_toolkit::{Algorithm, Proof, VerifiableCredential};
use multibase::Base;

let payload = unsigned_vc.signing_payload()?;    // JCS-canonical bytes to sign
let raw_signature: Vec<u8> = hsm_sign(&payload); // your HSM / external signer returns raw bytes

let proof = Proof::new_data_integrity(
    Algorithm::MlDsa65.cryptosuite(),
    multibase::encode(Base::Base58Btc, raw_signature),
);
let signed_vc = VerifiableCredential::from_parts(unsigned_vc, proof);
signed_vc.verify_auto(&public_key)?;

Sign with JSON Schema Validation

You can validate the credentialSubject against a JSON Schema before signing, ensuring the data conforms to an agreed-upon structure:

Schema validation is a separate, composable step: call validate with a SchemaSource, then sign. This keeps a single signing path regardless of where the schema comes from.

use verifiable_credential_toolkit::{Algorithm, SchemaSource, SigningKey, UnsignedVerifiableCredential};

let unsigned_vc: UnsignedVerifiableCredential =
    serde_json::from_str(&std::fs::read_to_string("credential.json").unwrap()).unwrap();
let signing_key = SigningKey::new(Algorithm::Ed25519, &std::fs::read("issuer.priv").unwrap())
    .expect("Invalid private key");

// Load the schema
let schema: serde_json::Value =
    serde_json::from_str(&std::fs::read_to_string("device_schema.json").unwrap()).unwrap();

// Validate then sign — validate fails (VcError::SchemaMismatch) if the
// credentialSubject doesn't match the schema.
unsigned_vc
    .validate(&SchemaSource::Inline(&schema))
    // Other schema sources:
    // .validate(&SchemaSource::Url("https://example.com/schemas/device.json"))  // fetched at validation time (native only)
    // .validate(&SchemaSource::None)                                            // no-op — skip validation
    .expect("Schema validation failed");
let signed_vc = unsigned_vc.sign(&signing_key).expect("Signing failed");

Customise the Proof

After signing, you can add metadata to the proof using either the builder pattern or direct field access:

use chrono::{Duration, Utc};
use url::Url;

let mut signed_vc = unsigned_vc.sign(&signing_key).unwrap();

// Builder pattern (consuming self, chainable).
signed_vc.proof = signed_vc.proof
    .set_verification_method("did:example:issuer#key-1".to_string())
    .set_created(Utc::now())
    .set_expires(Utc::now() + Duration::days(365))
    .set_domain(vec!["https://example.com".to_string()])
    .set_nonce(vec!["abc123".to_string()]);
    // Other setters:
    // .set_id(Url::parse("urn:uuid:...").unwrap())
    // .set_proof_purpose("authentication".to_string())
    // .set_challenge("challenge-from-verifier".to_string())
    // .set_previous_proof("urn:uuid:prior-proof".to_string())

// Or set fields directly:
// signed_vc.proof.created = Some(Utc::now());
// signed_vc.proof.domain = Some(vec!["https://example.com".to_string()]);

// Note: don't change `cryptosuite` or `proofValue` after signing — that would make
// the proof no longer match the signature.

Build a Verifiable Presentation

A Verifiable Presentation bundles one or more signed credentials together, for example when a holder wants to present multiple credentials to a verifier at once:

use verifiable_credential_toolkit::{VerifiableCredential, VerifiablePresentation};
use url::Url;

let vp = VerifiablePresentation {
    context: vec![Url::parse("https://www.w3.org/ns/credentials/v2").unwrap()],
    id: Some(Url::parse("urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5").unwrap()),
    presentation_type: vec!["VerifiablePresentation".to_string()],
    verifiable_credential: Some(vec![signed_vc]),
    holder: None,
    // A holder as a bare URL, or an object with extra properties:
    // holder: Some(Holder::Url(Url::parse("did:example:holder-123").unwrap())),
    // holder: Some(Holder::Object(HolderObject {
    //     id: Url::parse("did:example:holder-123").unwrap(),
    //     additional_properties: None,
    // })),
};

let vp_json = serde_json::to_string_pretty(&vp).unwrap();

Command-Line Tools

The toolkit includes two CLI tools for quick operations without writing code.

vc_signer (cargo default-run)

A CLI tool for signing Verifiable Credentials

Usage: vc_signer <COMMAND>

Commands:
  sign    Sign a verifiable credential
  verify  Verify a verifiable credential
  help    Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version
Sign a verifiable credential

Usage: vc_signer sign [OPTIONS] --input-vc <INPUT_VC> --key <KEY>

Options:
  -i, --input-vc <INPUT_VC>      Path to the unsigned VC JSON file
  -k, --key <KEY>                Path to the private key file
  -o, --output-vc <OUTPUT_VC>    Path to save the signed VC [default: signed_output.json]
  -s, --schema <SCHEMA>          Optional schema file path for validation
  -u, --schema-url <SCHEMA_URL>  Optional schema URL for validation
  -h, --help                     Print help
Verify a verifiable credential

Usage: vc_signer verify --input-vc <INPUT_VC> --key <KEY>

Options:
  -i, --input-vc <INPUT_VC>  Path to the signed VC JSON file
  -k, --key <KEY>            Path to the public key file
  -h, --help                 Print help

Examples:

# Basic signing
cargo run --bin vc_signer -- sign \
  --input-vc unsigned_credential.json \
  --key keys/issuer.priv \
  --output-vc signed_credential.json

# Sign with local schema validation
cargo run --bin vc_signer -- sign \
  --input-vc unsigned_credential.json \
  --key keys/issuer.priv \
  --schema schemas/device.json

# Sign with remote schema validation
cargo run --bin vc_signer -- sign \
  --input-vc unsigned_credential.json \
  --key keys/issuer.priv \
  --schema-url https://example.com/schemas/device.json

# Verify a signed credential
cargo run --bin vc_signer -- verify \
  --input-vc signed_credential.json \
  --key keys/issuer.pub

generate_keys

Generates Ed25519 key pairs

Usage: generate_keys [OPTIONS]

Options:
  -o, --output <OUTPUT>  Output directory for the keys [default: .]
  -h, --help             Print help
  -V, --version          Print version

Examples:

# Generate a keypair in the current directory
cargo run --bin generate_keys

# Generate a keypair in a specific directory
cargo run --bin generate_keys -- --output ./keys/

This creates two raw binary files:

  • {timestamp}.priv — 32-byte Ed25519 private key
  • {timestamp}.pub — 32-byte Ed25519 public key

JavaScript / TypeScript (WASM)

The library compiles to WebAssembly for use in browsers and Node.js. TypeScript type definitions are included (see verifiable_credential_toolkit.d.ts).

Build the WASM Package

# Compile to WASM (one command for both targets)
cargo build --target wasm32-unknown-unknown --release

# For browser usage (ES module with init() function)
wasm-bindgen --target web --out-dir pkg \
  target/wasm32-unknown-unknown/release/verifiable_credential_toolkit.wasm

# For Node.js usage (CommonJS-compatible)
wasm-bindgen --target nodejs --out-dir pkg \
  target/wasm32-unknown-unknown/release/verifiable_credential_toolkit.wasm

Browser Example

<!DOCTYPE html>
<html>
  <head>
    <title>VC Toolkit Demo</title>
  </head>
  <body>
    <h1>Verifiable Credential Demo</h1>
    <pre id="output"></pre>
    <script type="module">
      import init, {
        sign,
        verify,
        generate_keypair,
      } from "./pkg/verifiable_credential_toolkit.js";

      async function run() {
        // Initialise the WASM module (required for browser target)
        await init();

        // Generate a fresh keypair
        const keypair = generate_keypair();
        // Post-quantum: generate_keypair_for("ML-DSA-65"), then sign_with_algorithm +
        // verify_auto. CBOR/Protobuf helpers (sign_cbor_vc, …) are exported too.

        // Define a credential
        const unsignedVC = {
          "@context": ["https://www.w3.org/ns/credentials/v2"],
          type: ["VerifiableCredential"],
          issuer: "https://example.com/issuers/device-manufacturer",
          credentialSubject: {
            id: "urn:uuid:sensor-001",
            name: "Temperature Sensor Alpha",
          },
        };

        // Sign the credential
        const signedVC = sign(unsignedVC, keypair.signing_key());

        // Verify it
        const isValid = verify(signedVC, keypair.verifying_key());

        document.getElementById("output").textContent =
          `Signed VC:\n${JSON.stringify(signedVC, null, 2)}\n\nValid: ${isValid}`;
      }

      run().catch(console.error);
    </script>
  </body>
</html>

Node.js Example

import {
  sign,
  verify,
  verify_with_schema_check,
  generate_keypair,
} from "./pkg/verifiable_credential_toolkit.js";

// Generate keys (or load existing ones)
const keypair = generate_keypair();
// Post-quantum instead (label: "ML-DSA-44" | "ML-DSA-65" | "ML-DSA-87"):
// const keypair = generate_keypair_for("ML-DSA-65");

// Create a credential
const unsignedVC = {
  "@context": ["https://www.w3.org/ns/credentials/v2"],
  id: "urn:uuid:9a3e3c0e-2db0-412a-95c7-cf5520ba78df",
  type: ["VerifiableCredential", "DeviceCertificate"],
  issuer: "https://example.com/issuers/device-manufacturer",
  validFrom: "2024-01-01T00:00:00Z",
  credentialSchema: {
    id: "https://example.com/schemas/device.json",
    type: "JsonSchema",
  },
  credentialSubject: {
    id: "urn:uuid:device-001",
    name: "Temperature Sensor A",
  },
};

// Sign
const signedVC = sign(unsignedVC, keypair.signing_key());
console.log("Signed VC:", JSON.stringify(signedVC, null, 2));

// Verify (signature + validity period)
const isValid = verify(signedVC, keypair.verifying_key());
console.log("Signature valid:", isValid);

// Verify with schema validation
const schema = {
  type: "object",
  properties: {
    id: { type: "string" },
    name: { type: "string" },
  },
  required: ["id", "name"],
};

const isValidWithSchema = verify_with_schema_check(
  signedVC,
  keypair.verifying_key(),
  schema,
);
console.log("Valid with schema:", isValidWithSchema);

// ── Other options (import the functions you use) ─────────────────────────
// Any algorithm — pass a label and raw key bytes:
// const signedVC = sign_with_algorithm(unsignedVC, "ML-DSA-65", keypair.signing_key());
// const ok = verify_auto(signedVC, keypair.verifying_key());                    // reads the cryptosuite from the proof
// const ok = verify_with_algorithm(signedVC, "ML-DSA-65", keypair.verifying_key());
//
// CBOR / Protobuf (operate on Uint8Array bytes; signed once, verifies in any format):
// const cbor   = encode_unsigned_vc_to_cbor(unsignedVC);
// const signed = sign_cbor_vc(cbor, keypair.signing_key());                     // Ed25519
// const signed = sign_cbor_vc_with_algorithm(cbor, "ML-DSA-65", keypair.signing_key());
// const ok     = verify_cbor_vc_auto(signed, keypair.verifying_key());
// (…and encode_*_to_protobuf / sign_protobuf_vc[_with_algorithm] / verify_protobuf_vc_auto)

TypeScript Types

The package ships with a .d.ts file. Key exports:

// Key generation
function generate_keypair(): KeyPair;

// Signing and verification
function sign(
  unsigned_vc: UnsignedVerifiableCredential,
  private_key: Uint8Array,
): VerifiableCredential;
function verify(
  signed_vc: VerifiableCredential,
  public_key: Uint8Array,
): boolean;
function verify_with_schema_check(
  signed_vc: VerifiableCredential,
  public_key: Uint8Array,
  schema: any,
): boolean;

// KeyPair class
class KeyPair {
  signing_key(): Uint8Array; // 32-byte private key
  verifying_key(): Uint8Array; // 32-byte public key
}

API Reference

Rust API

| Function / Method | Description | | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | generate_keypair(Algorithm) → KeyPair | Generate a KeyPair { signing_key, verifying_key } for any algorithm | | generate_keypair_bytes(Algorithm) → (Vec<u8>, Vec<u8>) | Generate raw (private, public) key bytes (HSM / wasm / cross-language interop) | | SigningKey::new(Algorithm, &[u8]) → Result<SigningKey> | Wrap private-key bytes, length-checked against the algorithm | | VerifyingKey::new(Algorithm, &[u8]) → Result<VerifyingKey> | Wrap public-key bytes, length-checked against the algorithm | | UnsignedVerifiableCredential::builder(context, type, issuer, subject) → …Builder | Fluent builder; chain optional setters then .build() | | UnsignedVerifiableCredential::validate(&SchemaSource) → Result<()> | Validate credentialSubject against a SchemaSource (None / Inline / Url) | | UnsignedVerifiableCredential::sign(&SigningKey) → Result<VerifiableCredential> | Sign a credential; algorithm read from the key (call validate first for schema checks) | | UnsignedVerifiableCredential::sign_with_algorithm(Algorithm, &[u8]) → Result<VerifiableCredential> | Sign with an explicit algorithm and raw private-key bytes | | VerifiableCredential::validate(&SchemaSource) → Result<()> | Validate the embedded credentialSubject against a SchemaSource | | VerifiableCredential::verify(&VerifyingKey) → Result<()> | Verify signature + validity period; checks the proof's cryptosuite matches the key's algorithm | | VerifiableCredential::verify_auto(&[u8]) → Result<()> / verify_with_algorithm(Algorithm, &[u8]) | Verify from raw public-key bytes, dispatching on the proof's cryptosuite (or an explicit algorithm) | | VerifiableCredential::to_unsigned() → UnsignedVerifiableCredential | Strip the proof to get back an unsigned credential |

All fallible operations return Result<_, VcError> — a typed error enum you can match on (e.g. VcError::Expired, VcError::SchemaMismatch, VcError::SignatureVerificationFailed).

SigningKey and VerifyingKey are distinct types that each carry their Algorithm (Ed25519 or an ML-DSA parameter set), so a private and public key can never be swapped at a call site — the mismatch is a compile error — and sign / verify read the algorithm from the key.

SchemaSource selects where the JSON Schema comes from: SchemaSource::None, SchemaSource::Inline(&Value), or SchemaSource::Url(&str) (native only). To validate and sign in one expression: vc.validate(&schema).and_then(|()| vc.sign(&signing_key)).

Serialization formats (CBOR / Protobuf)

The bindings module abstracts wire formats behind the CredentialCodec trait. Each format (bindings::cbor::Cbor, bindings::protobuf::Protobuf) implements four bytes↔domain conversions; the sign/verify pipeline is provided once as default methods on the trait, so the format type is the entry point — there are no per-format free functions to learn. Because the signature is over the format-independent JCS canonical form, a credential signed in one format verifies in any other.

| Method (on Cbor / Protobuf) | Description | | --- | --- | | C::encode_unsigned(&Unsigned…) → Result<Vec<u8>> / C::encode_signed(&…) → Result<Vec<u8>> | Encode to this format's bytes | | C::decode_unsigned(&[u8]) / C::decode_signed(&[u8]) | Decode from this format's bytes | | C::sign(&[u8], Algorithm, &[u8]) → Result<Vec<u8>> | Decode unsigned bytes, sign with any cryptosuite, re-encode | | C::verify(&[u8], Algorithm, &[u8]) → Result<()> | Decode signed bytes and verify with an explicit algorithm | | C::verify_auto(&[u8], &[u8]) → Result<()> | Verify, reading the algorithm from the proof's cryptosuite |

let signed = Cbor::sign(&unsigned_cbor, Algorithm::MlDsa65, &private_key)?;
Cbor::verify_auto(&signed, &public_key)?;

WASM/JavaScript API

| Function | Description | | ----------------------------------------------------------------- | ------------------------------------------------------------------------ | | generate_keypair() → KeyPair | Generate a new Ed25519 keypair | | generate_keypair_for(algorithm) → KeyPair | Generate a keypair for any algorithm label (Ed25519, ML-DSA-44/65/87) | | sign(unsignedVC, privateKey) → VerifiableCredential | Sign a credential with Ed25519 (throws on error) | | verify(signedVC, publicKey) → boolean | Verify an Ed25519 signed credential | | sign_with_algorithm(unsignedVC, algorithm, privateKey) → VerifiableCredential | Sign with any algorithm and raw key bytes | | verify_with_algorithm(signedVC, algorithm, publicKey) → boolean | Verify with an explicit algorithm | | verify_auto(signedVC, publicKey) → boolean | Verify, reading the algorithm from the proof's cryptosuite | | verify_with_schema_check(signedVC, publicKey, schema) → boolean | Verify (Ed25519) with JSON Schema validation | | sign_cbor_vc(unsignedBytes, privateKey) → Uint8Array | Sign a CBOR-encoded credential with Ed25519 | | verify_cbor_vc(signedBytes, publicKey) → boolean | Verify a CBOR-encoded Ed25519 credential | | sign_cbor_vc_with_algorithm(unsignedBytes, algorithm, privateKey) → Uint8Array | Sign CBOR bytes with any algorithm | | verify_cbor_vc_auto(signedBytes, publicKey) → boolean | Verify CBOR bytes, reading the algorithm from the proof | | sign_protobuf_vc(unsignedBytes, privateKey) → Uint8Array | Sign a Protobuf-encoded credential with Ed25519 | | verify_protobuf_vc(signedBytes, publicKey) → boolean | Verify a Protobuf-encoded Ed25519 credential | | sign_protobuf_vc_with_algorithm(unsignedBytes, algorithm, privateKey) → Uint8Array | Sign Protobuf bytes with any algorithm | | verify_protobuf_vc_auto(signedBytes, publicKey) → boolean | Verify Protobuf bytes, reading the algorithm from the proof | | normalize_object(input) → any | Recursively strip undefined values from a JS object/array (null is preserved); useful before signing |

TypeScript types live in verifiable_credential_toolkit.d.ts. Keys use branded types (SigningKey / VerifyingKey) so a public and private key can't be swapped at a call site — KeyPair.signing_key() / .verifying_key() return the right brand, and raw bytes are branded with an assertion (bytes as SigningKey).


JSON Schema Validation

You can enforce a structure on credentialSubject using JSON Schema. This is useful when multiple parties agree on what fields a credential should contain.

Example schema for a device credential:

{
  "title": "Device",
  "description": "Schema for a device credential subject",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "Unique identifier for the device"
    },
    "name": {
      "type": "string",
      "description": "Human-readable device name"
    },
    "model": {
      "type": "string",
      "description": "Device model number"
    }
  },
  "required": ["id", "name"]
}

When you call validate with a SchemaSource (in Rust) or use verify_with_schema_check (in WASM/JavaScript), the credentialSubject is validated against this schema. If it doesn't match the required structure, the operation returns an error.


Examples

Working examples are included in the repository:

| Directory | Description | | ------------------------------------------------------------ | ---------------------------------------------------- | | wasm_js_example_usage/ | Browser-based signing and verification using WASM | | wasm_nodejs_example_usage/ | Node.js signing, verification, and schema validation | | examples/ | Rust examples (run with cargo run --example) |

Both WASM examples are TypeScript and exercise the full API — sign/verify, tamper and wrong-key rejection, schema validation, and the CBOR/Protobuf bindings. The Node example self-checks every result (non-zero exit on failure); the browser example renders results on the page.

Running the Node.js Example

cd wasm_nodejs_example_usage

# Builds the WASM, generates Node.js bindings, and writes pkg/package.json.
# wasm-bindgen's `nodejs` target emits CommonJS, but this dir is an ES module
# ("type": "module"), so pkg/ needs its own package.json declaring
# "type": "commonjs". `npm run build` does all of this.
npm run build

# Run it (Node >= 22 runs the .ts directly by stripping the types)
npm start          # or: node index.ts

If you build by hand instead of npm run build, remember the final step:

echo '{"type":"commonjs"}' > wasm_nodejs_example_usage/pkg/package.json

Running the Browser Example

cd wasm_js_example_usage

npm install        # one-time: installs the TypeScript compiler
npm run build      # builds WASM (--target web) and compiles index.ts -> index.js
npm run serve      # python3 -m http.server 8080
# Open http://localhost:8080 in your browser; results render on the page.

Running the Rust Examples

# End-to-end: generate keys, sign, verify, and inspect
cargo run --example full_workflow

# Signing with JSON Schema validation
cargo run --example schema_validation

Verifiable Presentations

A Verifiable Presentation (VP) is a wrapper that bundles one or more Verifiable Credentials for transmission to a verifier. Use cases include:

  • A device presenting both a manufacturer certificate and a calibration certificate.
  • A user presenting identity and access credentials together.
{
  "@context": ["https://www.w3.org/ns/credentials/v2"],
  "id": "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5",
  "type": ["VerifiablePresentation"],
  "verifiableCredential": [
    {
      /* signed VC 1 */
    },
    {
      /* signed VC 2 */
    }
  ],
  "holder": "did:example:holder-123"
}

Security Considerations

  • Protect private keys. Anyone with access to a private key can forge credentials. Store them securely (e.g. hardware security modules, encrypted storage).
  • Distribute public keys via trusted channels. Verifiers must be confident a public key genuinely belongs to the claimed issuer.
  • Set validity periods. Use validFrom and validUntil to limit the window during which a credential is accepted.
  • Use schema validation when you need to enforce a specific data structure on credentials.
  • Key rotation. Periodically generate new keypairs and re-issue credentials as needed.

License

Apache-2.0 — see Cargo.toml for details.