did-btcr2-js

@did-btcr2/cryptosuite

TypeScript implementation of the Data Integrity BIP340 Cryptosuite v0.1 specification.

Part of the did-btcr2-js monorepo.

Summary

This package produces and verifies the Data Integrity proofs that authenticate every did:btcr2 update. A signed update is a JSON document plus a proof block carrying a BIP-340 Schnorr signature over the JCS-canonicalized payload.

The cryptosuite is what links the abstract Signer interface from @did-btcr2/keypair to the on-disk shape of a signed DID update.

Install

npm install @did-btcr2/cryptosuite

Or with pnpm:

pnpm add @did-btcr2/cryptosuite

The package ships both ESM (dist/esm/) and CJS (dist/cjs/) via conditional exports, so it works with import and require out of the box.

Key Exports

Concern Entry point
Build / verify proofs BIP340Cryptosuite, Cryptosuite, VerificationResult
Proof primitive BIP340DataIntegrityProof, DataIntegrityProof, DataIntegrityProofObject
Update payload types UnsignedBTCR2Update, SignedBTCR2Update, BTCR2Update
Verification method wrapper SchnorrMultikey, Multikey, MultikeyObject
Construction helpers FromSecretKey, FromPublicKey
Cryptosuite config DataIntegrityConfig

Quick Start

Sign an update

import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
import { Secp256k1SecretKey } from '@did-btcr2/keypair';

const controller   = 'did:btcr2:k1q5p...';
const id           = '#initialKey';
const secretKeyBytes = new Secp256k1SecretKey(rawSecretBytes).bytes; // 32-byte Uint8Array
const multikey     = SchnorrMultikey.fromSecretKey(id, controller, secretKeyBytes);

const unsigned = { /* UnsignedBTCR2Update: @context, patch, sourceHash, targetHash, targetVersionId */ };
const config = {
  '@context'         : ['https://w3id.org/security/v2', 'https://w3id.org/zcap/v1', 'https://w3id.org/json-ld-patch/v1', 'https://btcr2.dev/context/v1'],
  type               : 'DataIntegrityProof' as const,
  cryptosuite        : 'bip340-jcs-2025',
  proofPurpose       : 'capabilityInvocation',
  verificationMethod : `${controller}${id}`,
};
const proof = multikey.toCryptosuite().createProof(unsigned, config);
// proof.cryptosuite === 'bip340-jcs-2025'

Verify a signed update

import { SchnorrMultikey } from '@did-btcr2/cryptosuite';

// Reconstruct the multikey from the DID verification method.
// verificationMethod is a DidVerificationMethod with id, controller, publicKeyMultibase.
const multikey = SchnorrMultikey.fromVerificationMethod(verificationMethod);
const result   = multikey.toCryptosuite().verifyProof(signedUpdate);

if (!result.verified) {
  throw new Error('DI proof failed');
}

Sign via an external Signer (HSM, KMS, hardware wallet)

import { SchnorrMultikey } from '@did-btcr2/cryptosuite';

// kmsSigner implements Signer: { publicKey: Uint8Array; sign(data, scheme): SignatureBytes }
const controller = 'did:btcr2:k1q5p...';
const id         = '#initialKey';
const multikey   = SchnorrMultikey.fromSigner(id, controller, kmsSigner);

const config = {
  '@context'         : ['https://w3id.org/security/v2', 'https://w3id.org/zcap/v1', 'https://w3id.org/json-ld-patch/v1', 'https://btcr2.dev/context/v1'],
  type               : 'DataIntegrityProof' as const,
  cryptosuite        : 'bip340-jcs-2025',
  proofPurpose       : 'capabilityInvocation',
  verificationMethod : `${controller}${id}`,
};
const proof = multikey.toCryptosuite().createProof(unsigned, config);

fromSigner seeds the multikey’s public key from kmsSigner.publicKey and delegates all signing to the signer, so no secret bytes ever enter the JS process. A public-key mismatch at construction time throws immediately.

Architecture Principles

Build & Test

# From packages/cryptosuite/
pnpm build              # Compile ESM + CJS + type declarations
pnpm build:tests        # Compile tests to tests/compiled/
pnpm test               # Run the test suite with coverage
pnpm lint               # ESLint (zero warnings tolerated)

Documentation

License

MPL-2.0