@scenesystems/sign
v0.1.0
Published
Digital signatures, key agreement, and key encapsulation for Effect
Readme
@scenesystems/sign
Digital signatures, key agreement, and key encapsulation for Effect. Classical and post-quantum algorithms, built on the Noble audited cryptographic ecosystem.
Installation
npm install @scenesystems/sign
# or
pnpm add @scenesystems/sign
# or
bun add @scenesystems/signRequires effect ≥ 3.20.0 as a peer dependency.
Why this package?
Digital signatures prove who sent a message. Key agreement lets two parties derive a shared secret. Key encapsulation does the same thing but with quantum resistance. @scenesystems/sign provides all three families through a single Effect-native API with typed errors, self-describing output types, and exhaustive algorithm dispatch — from Ed25519 to post-quantum ML-DSA and the XWing hybrid KEM.
- Ed25519 + secp256k1 — classical signatures for general use and blockchain compatibility
- ML-DSA + SLH-DSA — NIST post-quantum signatures (FIPS 204/205) at multiple security levels
- X25519 — elliptic-curve Diffie–Hellman key agreement
- XWing — hybrid KEM combining X25519 + ML-KEM-768 for quantum-resistant key transport
Algorithms
Signatures
| Algorithm | Family | Security | Signature size | Use case |
| ------------------- | --------------- | -------- | -------------- | ------------------------------ |
| ed25519 | EdDSA | 128-bit | 64 B | General purpose, fast |
| secp256k1-ecdsa | ECDSA | 128-bit | 64 B | Bitcoin/Ethereum compatibility |
| secp256k1-schnorr | Schnorr | 128-bit | 64 B | Taproot/batch verification |
| ml-dsa-44 | Lattice (PQ) | NIST 2 | 2,420 B | Post-quantum, smallest |
| ml-dsa-65 | Lattice (PQ) | NIST 3 | 3,309 B | Post-quantum, recommended |
| ml-dsa-87 | Lattice (PQ) | NIST 5 | 4,627 B | Post-quantum, highest security |
| slh-dsa-sha2-128f | Hash-based (PQ) | NIST 1 | ~7,856 B | Post-quantum, fast signing |
| slh-dsa-sha2-128s | Hash-based (PQ) | NIST 1 | ~7,856 B | Post-quantum, small signatures |
| slh-dsa-sha2-192f | Hash-based (PQ) | NIST 3 | ~16,224 B | Post-quantum, higher security |
| slh-dsa-sha2-256f | Hash-based (PQ) | NIST 5 | ~29,792 B | Post-quantum, maximum security |
Key agreement
| Algorithm | Family | Security | Shared secret |
| --------- | ------ | -------- | ------------- |
| x25519 | ECDH | 128-bit | 32 B |
Key encapsulation (KEM)
| Algorithm | Family | Security | Ciphertext |
| --------- | --------------- | --------- | ---------- |
| xwing | X25519 + ML-KEM | Hybrid PQ | ~1,121 B |
Choosing an algorithm
Use Ed25519 for general-purpose signing — it's fast, widely supported, and deterministic.
Use secp256k1 when interacting with Bitcoin, Ethereum, or other blockchain systems.
Use ML-DSA-65 if you need post-quantum signatures today. It's the NIST-recommended parameter set (FIPS 204) with a good balance of security and size.
Use SLH-DSA when you want conservative post-quantum security that doesn't rely on lattice hardness — it's based purely on hash functions. Signing is slow (1–5 seconds), so it's best suited for infrequent operations like certificate issuance.
Use XWing KEM for quantum-resistant key establishment. It combines X25519 and ML-KEM-768 — an attacker must break both to recover the shared secret.
Quick start
import { generateKeyPair, sign, utf8ToBytes, verify } from "@scenesystems/sign"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const keys = yield* generateKeyPair("ed25519")
const message = utf8ToBytes("hello, signatures!")
const sig = yield* sign("ed25519", message, keys.secretKey, keys.publicKey)
const valid = yield* verify(sig, message)
// true
})API
Signing
| Function | Description |
| ------------------------------------------------ | ------------------------------------------------------ |
| sign(algorithm, message, secretKey, publicKey) | Sign a message → Effect<Signature> |
| verify(signature, message) | Verify a self-describing signature → Effect<boolean> |
Key agreement
| Function | Description |
| ----------------------------------------------------- | ------------------------------------ |
| deriveSharedSecret(algorithm, secretKey, publicKey) | X25519 ECDH → Effect<SharedSecret> |
Key encapsulation
| Function | Description |
| ----------------------------------------------- | ------------------------------------- |
| encapsulate(algorithm, publicKey) | Encapsulate → Effect<KemCiphertext> |
| decapsulate(algorithm, ciphertext, secretKey) | Decapsulate → Effect<Uint8Array> |
Key generation
| Function | Description |
| ---------------------------- | --------------------------------------------------- |
| generateKeyPair(algorithm) | Generate keys for any algorithm → Effect<KeyPair> |
Encoding utilities
| Function | Description |
| ------------------ | -------------------------------------- |
| utf8ToBytes(str) | Convert a UTF-8 string to Uint8Array |
| toHex(bytes) | Encode bytes to lowercase hex string |
| equalBytes(a, b) | Constant-time byte array comparison |
Schema types
| Type | Description |
| -------------------- | -------------------------------------------------------- |
| SignatureAlgorithm | 10 signature algorithm literals |
| AgreementAlgorithm | "x25519" |
| KemAlgorithm | "xwing" |
| Signature | Schema.Class — algorithm, signature, publicKey |
| SharedSecret | Schema.Class — algorithm, sharedSecret |
| KemCiphertext | Schema.Class — algorithm, ciphertext, sharedSecret |
| KeyPair | Schema.Class — algorithm, publicKey, secretKey |
Errors
| Error | Raised by | Description |
| --------------------- | ----------------- | ------------------------------------ |
| SigningFailed | sign | Signing operation failed |
| VerificationFailed | verify | Signature is valid but doesn't match |
| InvalidSignature | verify | Signature data is malformed |
| KeyGenerationFailed | generateKeyPair | Key generation failed |
Examples
Sign and verify
import { generateKeyPair, sign, utf8ToBytes, verify } from "@scenesystems/sign"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const keys = yield* generateKeyPair("ed25519")
const message = utf8ToBytes("transfer 100 tokens")
const sig = yield* sign("ed25519", message, keys.secretKey, keys.publicKey)
const valid = yield* verify(sig, message)
// Tampered message fails verification
const tampered = utf8ToBytes("transfer 999 tokens")
const invalid = yield* verify(sig, tampered)
// false
})Key agreement with X25519
import { deriveSharedSecret, generateKeyPair } from "@scenesystems/sign"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const alice = yield* generateKeyPair("x25519")
const bob = yield* generateKeyPair("x25519")
// Both sides derive the same shared secret
const secretA = yield* deriveSharedSecret("x25519", alice.secretKey, bob.publicKey)
const secretB = yield* deriveSharedSecret("x25519", bob.secretKey, alice.publicKey)
// secretA.sharedSecret deep-equals secretB.sharedSecret
})Post-quantum KEM with XWing
import { decapsulate, encapsulate, generateKeyPair } from "@scenesystems/sign"
import { Effect } from "effect"
const program = Effect.gen(function* () {
// Recipient generates a hybrid key pair
const recipient = yield* generateKeyPair("xwing")
// Sender encapsulates a shared secret for the recipient
const { ciphertext, sharedSecret: senderSecret } = yield* encapsulate("xwing", recipient.publicKey)
// Recipient decapsulates to recover the same shared secret
const recipientSecret = yield* decapsulate("xwing", ciphertext, recipient.secretKey)
// senderSecret deep-equals recipientSecret
})Post-quantum signatures
import { generateKeyPair, sign, utf8ToBytes, verify } from "@scenesystems/sign"
import { Effect } from "effect"
const program = Effect.gen(function* () {
// ML-DSA-65 — NIST FIPS 204, recommended parameter set
const keys = yield* generateKeyPair("ml-dsa-65")
const message = utf8ToBytes("quantum-resistant document")
const sig = yield* sign("ml-dsa-65", message, keys.secretKey, keys.publicKey)
const valid = yield* verify(sig, message)
// true — verified with post-quantum security
})Error handling
import { generateKeyPair, sign, utf8ToBytes, verify } from "@scenesystems/sign"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const keys = yield* generateKeyPair("ed25519").pipe(
Effect.catchTag("KeyGenerationFailed", (e) => Effect.die(`keygen failed: ${e.reason}`))
)
const message = utf8ToBytes("hello")
const sig = yield* sign("ed25519", message, keys.secretKey, keys.publicKey).pipe(
Effect.catchTag("SigningFailed", (e) => Effect.die(`signing failed: ${e.reason}`))
)
const valid = yield* verify(sig, message).pipe(Effect.catchTag("VerificationFailed", (e) => Effect.succeed(false)))
})See the examples/ directory for complete runnable programs.
Cryptographic foundations
All primitives wrap the Noble cryptographic ecosystem — independently audited by Cure53 and Trail of Bits, zero-dependency, high-performance pure JavaScript implementations.
| Dependency | Audits | Purpose |
| --------------------- | -------- | ----------------------------------------------------- |
| @noble/curves | 6 audits | Ed25519, secp256k1, X25519 |
| @noble/hashes | 6 audits | SHA-256/512 for key encoding |
| @noble/post-quantum | 1 audit | ML-DSA (FIPS 204), SLH-DSA (FIPS 205), ML-KEM (XWing) |
Standards
| Algorithm | Specification | | ------------------- | ------------------------------------------------------------------------- | | Ed25519 | RFC 8032 | | X25519 | RFC 7748 | | secp256k1 | SEC 2 §2.4.1 | | Schnorr (secp256k1) | BIP-340 | | ML-DSA | NIST FIPS 204 | | SLH-DSA | NIST FIPS 205 | | ML-KEM (via XWing) | NIST FIPS 203 | | XWing | Barbosa et al. (2024) |
License
MIT — Copyright © 2026 Scene Systems
