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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@substrate-system/frost

v0.0.9

Published

Secure key backup & threshold signatures

Downloads

21

Readme

FROST

tests types module semantic versioning Common Changelog install size gzip size license

Two use-cases: private key backup and threshold signatures.

This is a TypeScript implementation of the FROST threshold signature scheme as specified in RFC 9591.


FROST (Flexible Round-Optimized Schnorr Threshold signatures) is a threshold signature scheme that allows a group of participants to collectively generate signatures, requiring a minimum number of participants during the signing process.

A single private key gets split into multiple shards during setup. Each participant gets one shard of the key. The original private key can be discarded/lost at this point.

The participants use their individual key shards to collectively create signatures that are mathematically equivalent to what the original private key would have produced, but the original private key itself is never reconstructed.

Even after successful signing ceremonies, no single participant ever gains access to the complete private key. The threshold property is maintained permanently — you always need the minimum number of participants to create future signatures.

Featuring:

  • Simple Key Backup: Split any Ed25519 key with split(), recover with recover()
  • Easy Signing: Sign with recovered keys using sign() - no ceremony complexity
  • Flexible Input: Accepts CryptoKey, PKCS#8, or raw 32-byte keys
  • Threshold Signatures: Configurable m-of-n threshold signing for advanced use cases
  • RFC 9591 Compliant: See the doc

Installation

npm i -S @substrate-system/frost

Example

Key Backup and Recovery

FROST can be used to backup an existing Ed25519 private key by splitting it into threshold shares. This is useful for creating secure key storage where you need multiple shares to recover the original key.

In Dark Crystal, for example, the intended use is to give the shards of your private key to several of your friends, using the social graph to securely backup your key. But this works just as well by distributing your key shards amongst multiple of your own devices, in case you lose one device.

[!NOTE]
We do not create a CryptoKey in recover.

The value returned by recover() is a scalar (the mathematical secret used in signing), not a seed. WebCrypto's importKey expects a seed, which it then hashes with SHA-512 and bit-clamps to derive a scalar. Since we can't reverse this one-way process, we can't convert our recovered scalar back into a CryptoKey. Instead, use the sign() function, which handles the FROST signing ceremony internally using the scalar directly. Signatures from sign() will verify correctly with the original public key.

import { webcrypto } from 'crypto'
import {
    createFrostConfig,
    split,
    recover,
    sign
} from '@substrate-system/frost'

// 1. Generate or use existing Ed25519 keypair
const keyPair = await webcrypto.subtle.generateKey(
    { name: 'Ed25519' },
    true,  // extractable so we can split the private key
    ['sign', 'verify']
)

// 2. Split into 3 shares (require 2 to recover)
const config = createFrostConfig(2, 3)
const { groupPublicKey, keyPackages } = await split(
    keyPair.privateKey,
    config
)

// 3. Distribute shares to different locations
// - Share 1: USB drive in safe
// - Share 2: Cloud backup (encrypted)
// - Share 3: Paper backup

// 4. Later, recover using any 2 of 3 shares
const availableShares = [keyPackages[0], keyPackages[2]]
const recoveredKey = recover(availableShares, config)

// 5. Use the recovered key to sign
const message = new TextEncoder().encode('Important message')
const signature = await sign(recoveredKey, message, config)

// 6. Verify the signature with the original public key
const isValid = await webcrypto.subtle.verify(
    'Ed25519',
    keyPair.publicKey,
    signature,
    message
)

[!NOTE]

  • split accepts CryptoKey, Uint8Array (PKCS#8), or Uint8Array (32-byte raw scalar)
  • The recovered key will produce the same public key as the original
  • You need at least the threshold number of shares to recover
  • Different combinations of shares all recover the same key

Distributed Threshold Signing

Collaboratively sign a message. The final signature reveals only that the threshold was met. It does not reveal who signed. It is cryptographically impossible to determine which participants signed.

import {
  createFrostConfig,
  generateKeys,
  thresholdSign
} from '@substrate-system/frost'

// 1. Alice creates a 3-of-4 FROST setup
const config = createFrostConfig(3, 4)  // Need 3 out of 4 to sign
const { groupPublicKey, keyPackages } = generateKeys(config)

// 2. Distribute key packages to participants
const [aliceKey, bobKey, carolKey, desmondKey] = keyPackages

// 3. Later, any 3 participants can create a signature
const message = new TextEncoder().encode('Hello, FROST!')
const signature = await thresholdSign(
    [bobKey, carolKey, desmondKey],  // Any 3 participants
    message,
    groupPublicKey,
    config
)

// 4. Verify signature
const isValid = await crypto.subtle.verify(
    'Ed25519',
    new Uint8Array(groupPublicKey.point),
    signature,
    message
)

Try it

Run the example locally.

npm run example:node

This will execute the complete example showing:

  1. Alice creating a 3-of-4 threshold keypair
  2. Getting key shares for Alice, Bob, Carol, and Desmond
  3. Using any 3 participants to create threshold signatures
  4. Verifying the signature is valid

Test

Run the tests:

npm test

Start the example:

npm start

API

Key Backup

createFrostConfig

Creates a FROST configuration with Ed25519 cipher suite.

function createFrostConfig (
  minSigners: number,
  maxSigners: number
): FrostConfig
const config = createFrostConfig(2, 3)  // 2-of-3 threshold

split

async function split (
  privateKey: CryptoKey | Uint8Array,
  config: FrostConfig
): Promise<Signers>
const { groupPublicKey, keyPackages } = await split(keyPair.privateKey, config)

recover

Recover the private key from threshold shares.

function recover (
  keyPackages: KeyPackage[],
  config: FrostConfig
): Uint8Array
const recoveredKey = recover(keyPackages.slice(0, 2), config)

sign

Sign a message with a recovered key.

async function sign (
  recoveredKey:Uint8Array,
  message:Uint8Array,
  config:FrostConfig
):Promise<Uint8Array<ArrayBuffer>>
const signature = await sign(recoveredKey, message, config)

thresholdSign

Create a threshold signature from multiple participants.

async function thresholdSign (
  keyPackages:KeyPackage[],
  message:Uint8Array,
  groupPublicKey:GroupElement,
  config:FrostConfig
):Promise<Uint8Array>
const signature = await thresholdSign(
    [aliceKey, bobKey, carolKey],  // Participant key packages
    message,
    groupPublicKey,
    config
)

Distributed Signing

generateKeys

Generate keys for all participants.

function generateKeys (config:FrostConfig):Signers
const { groupPublicKey, keyPackages } = generateKeys(config)
// groupPublicKey: The collective public key
// keyPackages: Individual key packages for each participant

verifyKeyPackage

Verifies that a key package is valid.

function verifyKeyPackage (
  keyPackage:KeyPackage,
  config:FrostConfig
):boolean
const isValid = verifyKeyPackage(keyPackage, config)

Standards

This implementation follows:

  • RFC 9591 - The Flexible Round-Optimized Schnorr Threshold (FROST) Protocol

See Also


Internals

Signing Protocol

FrostSigner

Represents an individual participant in the signing ceremony.

const signer = new FrostSigner(keyPackage, config)

// Round 1: Generate nonce commitments
const round1 = signer.sign_round1()

// Round 2: Generate signature share
const round2 = signer.sign_round2(signingPackage, round1.nonces)

FrostCoordinator

Manages the signing ceremony and aggregates signatures.

const coordinator = new FrostCoordinator(config)

// Create signing package
const signingPackage = coordinator.createSigningPackage(
    message,
    commitmentShares,
    participantIds
)

// Aggregate signature shares
const signature = coordinator.aggregateSignatures(
    signingPackage,
    signatureShares
)

// Verify signature
const isValid = coordinator.verify(signature, message, groupPublicKey)

Protocol Flow

The FROST protocol consists of the following phases:

1. Key Generation (Setup)

// Generate keys for all participants
const { groupPublicKey, keyPackages } = generateKeys(config)

// Distribute key packages to participants securely

2. Signing Ceremony

Round 1: Commitment Phase

Each participant generates nonces and creates commitments:

const round1Results = signers.map(signer => signer.sign_round1())

Round 2: Signature Share Generation

Participants receive the signing package and generate signature shares:

const signingPackage = coordinator.createSigningPackage(
    message, commitmentShares, participantIds
)

const signatureShares = signers.map((signer, i) =>
    signer.sign_round2(signingPackage, round1Results[i].nonces)
)

Aggregation

The coordinator combines signature shares into a final signature:

const signature = coordinator.aggregateSignatures(
    signingPackage,
    signatureShares.map(r => r.signatureShare)
)

Step-by-Step Guide

Example

Alice can create a threshold keypair and later create signatures with her trusted friends.

Step 1: Alice Creates the Initial Setup

import {
    createFrostConfig,
    generateKeys,
    FrostCoordinator,
    FrostSigner
} from '@substrate-system/frost'

// Alice decides she wants a 3-of-4 threshold scheme
const config = createFrostConfig(3, 4)  // Need 3 out of 4 to sign
const { groupPublicKey, keyPackages } = generateKeys(config)

// Distribute key shares to Alice, Bob, Carol, and Desmond
const [aliceKey, bobKey, carolKey, desmondKey] = keyPackages

Step 2: Create a Signature

Later, Alice wants to sign a message but needs help from 3 of her 4 trusted friends:

// Alice chooses Carol and Desmond to help (any 3 would work)
const participants = [aliceKey, carolKey, desmondKey]
const signers = participants.map(pkg => new FrostSigner(pkg, config))
const coordinator = new FrostCoordinator(config)

Step 3: Sign

This process creates a threshold signature:

const message = new TextEncoder().encode('Alice\'s important message')

// Round 1: Each participant generates commitments
const round1 = signers.map(s => s.sign_round1())
const commitmentShares = round1.map((r, i) => ({
    participantId: participants[i].participantId,
    commitment: r.commitment
}))

// Create the signing package
const participantIds = participants.map(p => p.participantId)
const signingPackage = await coordinator.createSigningPackage(
    message,
    commitmentShares,
    participantIds
)

// Round 2: Generate signature shares
const signatureShares = []
for (let i = 0; i < signers.length; i++) {
    const res = await signers[i].sign_round2(signingPackage, round1[i].nonces)
    signatureShares.push(res.signatureShare)
}

// Combine into final signature
const finalSignature = coordinator.aggregateSignatures(
  signingPackage,
  signatureShares
)

// Verify it worked
const valid = await coordinator.verify(finalSignature, message, groupPublicKey)
console.log('Threshold signature valid:', valid)  // Should be true

The signature is mathematically equivalent to a single-key signature