feat(crypto-wasm): add deterministic Dilithium3 key derivation and hybrid signatures
This commit enables full wallet recovery from BIP-39 mnemonics by implementing deterministic Dilithium3 key derivation using HKDF-SHA3-256 with domain separation. Changes: - crates/synor-crypto-wasm: Implement deterministic Dilithium keygen - Use HKDF with info="synor:dilithium:v1" for key derivation - Enable pqc_dilithium's crypto_sign_keypair via dilithium_kat cfg flag - Add proper memory zeroization on drop - Add tests for deterministic key generation - apps/web: Update transaction signing for hybrid signatures - Add signTransactionHybrid() for Ed25519 + Dilithium3 signatures - Add createSendTransactionHybrid() for quantum-resistant transactions - Update fee estimation for larger hybrid signature size (~5.5KB/input) - Maintain legacy Ed25519-only functions for backwards compatibility - WASM module: Rebuild with deterministic keygen - Update synor_crypto_bg.wasm with new implementation - Module size reduced to ~470KB (optimized) - Documentation updates: - Update mobile wallet plan: React Native -> Flutter - Add testnet-first approach note - Update explorer frontend progress to 90%
This commit is contained in:
parent
6b5a232a5e
commit
3041c6d654
11 changed files with 766 additions and 310 deletions
13
.cargo/config.toml
Normal file
13
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Cargo configuration for Synor blockchain
|
||||
#
|
||||
# This file configures the Rust build system with project-specific settings.
|
||||
|
||||
# Enable pqc_dilithium's internal key generation for deterministic Dilithium keys
|
||||
# This allows us to generate Dilithium keypairs from a seed (mnemonic-derived)
|
||||
# which is essential for wallet recovery.
|
||||
[build]
|
||||
rustflags = ["--cfg", "dilithium_kat"]
|
||||
|
||||
# WASM target-specific settings for web wallet
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ["--cfg", "dilithium_kat"]
|
||||
|
|
@ -1,16 +1,41 @@
|
|||
/**
|
||||
* Transaction building utilities.
|
||||
*
|
||||
* ## Hybrid Signature Support
|
||||
*
|
||||
* Synor transactions require hybrid signatures combining:
|
||||
* - Ed25519 (classical, 64 bytes)
|
||||
* - Dilithium3/ML-DSA-65 (post-quantum, ~3.3KB)
|
||||
*
|
||||
* Both signatures must verify for a transaction to be valid.
|
||||
* This provides defense-in-depth against both classical and quantum attacks.
|
||||
*/
|
||||
|
||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
||||
import { hash, sign, type Keypair } from './crypto';
|
||||
import {
|
||||
hash,
|
||||
type Keypair,
|
||||
type HybridSignature,
|
||||
createHybridSignatureLocal,
|
||||
createHybridSignatureSmart,
|
||||
serializeHybridSignature,
|
||||
type SigningConfig,
|
||||
} from './crypto';
|
||||
import { getClient, type Utxo } from './rpc';
|
||||
|
||||
export interface TxInput {
|
||||
previousTxId: string;
|
||||
outputIndex: number;
|
||||
/** Ed25519 signature (64 bytes hex) - for backwards compatibility */
|
||||
signature?: string;
|
||||
/** Hybrid signature containing both Ed25519 and Dilithium components */
|
||||
hybridSignature?: {
|
||||
ed25519: string;
|
||||
dilithium: string;
|
||||
};
|
||||
publicKey?: string;
|
||||
/** Dilithium public key (1952 bytes hex) - required for hybrid verification */
|
||||
dilithiumPublicKey?: string;
|
||||
}
|
||||
|
||||
export interface TxOutput {
|
||||
|
|
@ -27,6 +52,8 @@ export interface UnsignedTransaction {
|
|||
|
||||
export interface SignedTransaction extends UnsignedTransaction {
|
||||
id: string;
|
||||
/** Indicates if this transaction uses hybrid signatures */
|
||||
isHybrid?: boolean;
|
||||
}
|
||||
|
||||
// Synor uses 8 decimal places (like satoshis)
|
||||
|
|
@ -55,11 +82,14 @@ export function fromSomas(somas: bigint): string {
|
|||
/**
|
||||
* Select UTXOs for a transaction using simple accumulator.
|
||||
* Returns selected UTXOs and change amount.
|
||||
*
|
||||
* For hybrid signatures, we account for the larger signature size (~3.4KB per input)
|
||||
*/
|
||||
export function selectUtxos(
|
||||
utxos: Utxo[],
|
||||
targetAmount: bigint,
|
||||
feePerByte: bigint = BigInt(1)
|
||||
feePerByte: bigint = BigInt(1),
|
||||
isHybrid: boolean = true
|
||||
): { selected: Utxo[]; change: bigint; fee: bigint } | null {
|
||||
// Sort by amount descending for efficiency
|
||||
const sorted = [...utxos].sort((a, b) => {
|
||||
|
|
@ -71,13 +101,18 @@ export function selectUtxos(
|
|||
const selected: Utxo[] = [];
|
||||
let accumulated = BigInt(0);
|
||||
|
||||
// Signature size: Ed25519 = 64 bytes, Dilithium = 3293 bytes
|
||||
// Public key size: Ed25519 = 32 bytes, Dilithium = 1952 bytes
|
||||
const inputSize = isHybrid
|
||||
? 150 + 64 + 3293 + 32 + 1952 // ~5.5KB per hybrid input
|
||||
: 150; // ~150 bytes for Ed25519-only
|
||||
|
||||
for (const utxo of sorted) {
|
||||
selected.push(utxo);
|
||||
accumulated += toSomas(utxo.amount);
|
||||
|
||||
// Estimate fee based on tx size
|
||||
// ~150 bytes per input, ~34 bytes per output, ~10 bytes overhead
|
||||
const estimatedSize = BigInt(selected.length * 150 + 2 * 34 + 10);
|
||||
const estimatedSize = BigInt(selected.length * inputSize + 2 * 34 + 10);
|
||||
const fee = estimatedSize * feePerByte;
|
||||
const totalNeeded = targetAmount + fee;
|
||||
|
||||
|
|
@ -182,19 +217,75 @@ export function serializeForSigning(tx: UnsignedTransaction): Uint8Array {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sign a transaction.
|
||||
* Sign a transaction with hybrid signatures (Ed25519 + Dilithium3).
|
||||
*
|
||||
* This is the recommended signing method for quantum-resistant transactions.
|
||||
* Both signatures must verify for the transaction to be valid on the network.
|
||||
*
|
||||
* @param tx - The unsigned transaction to sign
|
||||
* @param seed - The 64-byte BIP-39 seed for key derivation
|
||||
* @param ed25519PublicKey - The Ed25519 public key bytes
|
||||
* @param dilithiumPublicKey - The Dilithium public key bytes (1952 bytes)
|
||||
* @param config - Optional signing configuration
|
||||
*/
|
||||
export async function signTransactionHybrid(
|
||||
tx: UnsignedTransaction,
|
||||
seed: Uint8Array,
|
||||
ed25519PublicKey: Uint8Array,
|
||||
dilithiumPublicKey: Uint8Array,
|
||||
config?: SigningConfig
|
||||
): Promise<SignedTransaction> {
|
||||
const serialized = serializeForSigning(tx);
|
||||
const txHash = hash(serialized);
|
||||
|
||||
// Sign each input with hybrid signatures
|
||||
const signedInputs: TxInput[] = [];
|
||||
for (const input of tx.inputs) {
|
||||
// Create hybrid signature (Ed25519 + Dilithium)
|
||||
const hybridSig = config
|
||||
? await createHybridSignatureSmart(txHash, seed, config)
|
||||
: await createHybridSignatureLocal(txHash, seed);
|
||||
|
||||
signedInputs.push({
|
||||
...input,
|
||||
// Include both signature formats for compatibility
|
||||
signature: bytesToHex(hybridSig.ed25519),
|
||||
hybridSignature: {
|
||||
ed25519: bytesToHex(hybridSig.ed25519),
|
||||
dilithium: bytesToHex(hybridSig.dilithium),
|
||||
},
|
||||
publicKey: bytesToHex(ed25519PublicKey),
|
||||
dilithiumPublicKey: bytesToHex(dilithiumPublicKey),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...tx,
|
||||
inputs: signedInputs,
|
||||
id: bytesToHex(txHash),
|
||||
isHybrid: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a transaction with Ed25519 only (legacy mode).
|
||||
*
|
||||
* @deprecated Use signTransactionHybrid for quantum-resistant transactions.
|
||||
* This function is provided for backwards compatibility only.
|
||||
*/
|
||||
export async function signTransaction(
|
||||
tx: UnsignedTransaction,
|
||||
keypair: Keypair
|
||||
): Promise<SignedTransaction> {
|
||||
// Import the sign function for Ed25519-only signing
|
||||
const { sign } = await import('./crypto');
|
||||
|
||||
const serialized = serializeForSigning(tx);
|
||||
const txHash = hash(serialized);
|
||||
|
||||
// Sign each input
|
||||
// Sign each input with Ed25519 only
|
||||
const signedInputs: TxInput[] = [];
|
||||
for (const input of tx.inputs) {
|
||||
// Create signing message: txHash || inputIndex
|
||||
const signature = await sign(txHash, keypair.privateKey);
|
||||
|
||||
signedInputs.push({
|
||||
|
|
@ -208,6 +299,7 @@ export async function signTransaction(
|
|||
...tx,
|
||||
inputs: signedInputs,
|
||||
id: bytesToHex(txHash),
|
||||
isHybrid: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -215,21 +307,87 @@ export async function signTransaction(
|
|||
* Serialize signed transaction for submission.
|
||||
*/
|
||||
export function serializeTransaction(tx: SignedTransaction): string {
|
||||
// Simplified serialization - in production this would match
|
||||
// the exact binary format expected by the node
|
||||
return JSON.stringify({
|
||||
version: tx.version,
|
||||
inputs: tx.inputs,
|
||||
inputs: tx.inputs.map((input) => ({
|
||||
previousTxId: input.previousTxId,
|
||||
outputIndex: input.outputIndex,
|
||||
signature: input.signature,
|
||||
hybridSignature: input.hybridSignature,
|
||||
publicKey: input.publicKey,
|
||||
dilithiumPublicKey: input.dilithiumPublicKey,
|
||||
})),
|
||||
outputs: tx.outputs.map((o) => ({
|
||||
address: o.address,
|
||||
amount: o.amount.toString(),
|
||||
})),
|
||||
lockTime: tx.lockTime,
|
||||
isHybrid: tx.isHybrid,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level send function.
|
||||
* Wallet data with both Ed25519 and Dilithium keys.
|
||||
*/
|
||||
export interface WalletWithDilithium {
|
||||
seed: Uint8Array;
|
||||
keypair: Keypair;
|
||||
address: string;
|
||||
dilithiumPublicKey: Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level send function with hybrid signatures.
|
||||
*
|
||||
* This is the recommended way to send transactions for quantum resistance.
|
||||
*
|
||||
* @param fromAddress - The sender's address
|
||||
* @param toAddress - The recipient's address
|
||||
* @param amount - Amount to send in human-readable format
|
||||
* @param wallet - Wallet containing seed, keypair, and Dilithium public key
|
||||
* @param config - Optional signing configuration
|
||||
*/
|
||||
export async function createSendTransactionHybrid(
|
||||
fromAddress: string,
|
||||
toAddress: string,
|
||||
amount: string,
|
||||
wallet: WalletWithDilithium,
|
||||
config?: SigningConfig
|
||||
): Promise<SignedTransaction> {
|
||||
const client = getClient();
|
||||
|
||||
// Get UTXOs
|
||||
const utxos = await client.getUtxos(fromAddress);
|
||||
const targetAmount = toSomas(amount);
|
||||
|
||||
// Select UTXOs (account for larger hybrid signature size)
|
||||
const selection = selectUtxos(utxos, targetAmount, BigInt(1), true);
|
||||
if (!selection) {
|
||||
throw new Error('Insufficient funds');
|
||||
}
|
||||
|
||||
// Build transaction
|
||||
const tx = buildTransaction(
|
||||
selection.selected.map((utxo) => ({ utxo })),
|
||||
[{ address: toAddress, amount: targetAmount }],
|
||||
fromAddress,
|
||||
selection.change
|
||||
);
|
||||
|
||||
// Sign with hybrid signatures
|
||||
return signTransactionHybrid(
|
||||
tx,
|
||||
wallet.seed,
|
||||
wallet.keypair.publicKey,
|
||||
wallet.dilithiumPublicKey,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level send function (legacy Ed25519 only).
|
||||
*
|
||||
* @deprecated Use createSendTransactionHybrid for quantum-resistant transactions.
|
||||
*/
|
||||
export async function createSendTransaction(
|
||||
fromAddress: string,
|
||||
|
|
@ -243,8 +401,8 @@ export async function createSendTransaction(
|
|||
const utxos = await client.getUtxos(fromAddress);
|
||||
const targetAmount = toSomas(amount);
|
||||
|
||||
// Select UTXOs
|
||||
const selection = selectUtxos(utxos, targetAmount);
|
||||
// Select UTXOs (Ed25519-only mode)
|
||||
const selection = selectUtxos(utxos, targetAmount, BigInt(1), false);
|
||||
if (!selection) {
|
||||
throw new Error('Insufficient funds');
|
||||
}
|
||||
|
|
|
|||
104
apps/web/src/wasm/synor_crypto.d.ts
vendored
104
apps/web/src/wasm/synor_crypto.d.ts
vendored
|
|
@ -5,27 +5,35 @@ export class DilithiumSigningKey {
|
|||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Generate a new random Dilithium3 keypair.
|
||||
*/
|
||||
constructor();
|
||||
/**
|
||||
* Create a keypair from a 32-byte seed.
|
||||
*
|
||||
* The seed is expanded to generate the full keypair deterministically.
|
||||
* This allows recovery of keys from a mnemonic-derived seed.
|
||||
*/
|
||||
static fromSeed(seed: Uint8Array): DilithiumSigningKey;
|
||||
/**
|
||||
* Get the public key bytes.
|
||||
* Get the public key bytes (1952 bytes for Dilithium3).
|
||||
*/
|
||||
publicKey(): Uint8Array;
|
||||
/**
|
||||
* Get the secret key bytes.
|
||||
* Get the secret key bytes (4000 bytes for Dilithium3).
|
||||
*
|
||||
* WARNING: Handle with care! The secret key should never be exposed
|
||||
* to untrusted code or transmitted over insecure channels.
|
||||
*/
|
||||
secretKey(): Uint8Array;
|
||||
/**
|
||||
* Get the signature size in bytes.
|
||||
*/
|
||||
static signatureSize(): number;
|
||||
/**
|
||||
* Get the public key size in bytes.
|
||||
*/
|
||||
static publicKeySize(): number;
|
||||
/**
|
||||
* Get the secret key size in bytes.
|
||||
*/
|
||||
static secretKeySize(): number;
|
||||
/**
|
||||
* Generate a new random Dilithium3 keypair.
|
||||
*
|
||||
* This creates a new keypair using system entropy. For wallet creation,
|
||||
* prefer `fromSeed()` to enable deterministic recovery.
|
||||
*/
|
||||
constructor();
|
||||
/**
|
||||
* Sign a message with the Dilithium3 secret key.
|
||||
*
|
||||
|
|
@ -39,30 +47,28 @@ export class DilithiumSigningKey {
|
|||
*/
|
||||
verify(message: Uint8Array, signature: Uint8Array): boolean;
|
||||
/**
|
||||
* Get the public key size in bytes.
|
||||
* Create a keypair from a seed (32+ bytes).
|
||||
*
|
||||
* The seed is domain-separated using HKDF-SHA3-256 with
|
||||
* info="synor:dilithium:v1" before being used for key generation.
|
||||
* This ensures the same mnemonic produces the same keypair.
|
||||
*
|
||||
* ## Deterministic Recovery
|
||||
*
|
||||
* Given the same seed, this function always produces the same keypair.
|
||||
* This is essential for wallet recovery from BIP-39 mnemonics.
|
||||
*
|
||||
* ## Parameters
|
||||
*
|
||||
* * `seed` - At least 32 bytes, typically from a BIP-39 mnemonic seed.
|
||||
* For best security, use the full 64-byte BIP-39 seed.
|
||||
*/
|
||||
static publicKeySize(): number;
|
||||
/**
|
||||
* Get the secret key size in bytes.
|
||||
*/
|
||||
static secretKeySize(): number;
|
||||
/**
|
||||
* Get the signature size in bytes.
|
||||
*/
|
||||
static signatureSize(): number;
|
||||
static fromSeed(seed: Uint8Array): DilithiumSigningKey;
|
||||
}
|
||||
|
||||
export class Keypair {
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Generate a new random keypair.
|
||||
*/
|
||||
constructor();
|
||||
/**
|
||||
* Create a keypair from a 32-byte seed.
|
||||
*/
|
||||
static fromSeed(seed: Uint8Array): Keypair;
|
||||
/**
|
||||
* Create a keypair from a BIP-39 mnemonic phrase.
|
||||
*/
|
||||
|
|
@ -76,9 +82,9 @@ export class Keypair {
|
|||
*/
|
||||
publicKeyBytes(): Uint8Array;
|
||||
/**
|
||||
* Get the Synor address for this keypair.
|
||||
* Generate a new random keypair.
|
||||
*/
|
||||
address(network: string): string;
|
||||
constructor();
|
||||
/**
|
||||
* Sign a message.
|
||||
*/
|
||||
|
|
@ -87,6 +93,14 @@ export class Keypair {
|
|||
* Verify a signature.
|
||||
*/
|
||||
verify(message: Uint8Array, signature: Uint8Array): boolean;
|
||||
/**
|
||||
* Get the Synor address for this keypair.
|
||||
*/
|
||||
address(network: string): string;
|
||||
/**
|
||||
* Create a keypair from a 32-byte seed.
|
||||
*/
|
||||
static fromSeed(seed: Uint8Array): Keypair;
|
||||
}
|
||||
|
||||
export class Keys {
|
||||
|
|
@ -102,37 +116,37 @@ export class Mnemonic {
|
|||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Generate a new random mnemonic with the specified word count.
|
||||
* Get the word count.
|
||||
*/
|
||||
constructor(word_count: number);
|
||||
/**
|
||||
* Generate a 24-word mnemonic.
|
||||
*/
|
||||
static generate(word_count: number): Mnemonic;
|
||||
wordCount(): number;
|
||||
/**
|
||||
* Parse a mnemonic from a phrase.
|
||||
*/
|
||||
static fromPhrase(phrase: string): Mnemonic;
|
||||
/**
|
||||
* Get the mnemonic phrase as a string.
|
||||
* Generate a new random mnemonic with the specified word count.
|
||||
*/
|
||||
phrase(): string;
|
||||
constructor(word_count: number);
|
||||
/**
|
||||
* Get the mnemonic words as an array.
|
||||
*/
|
||||
words(): string[];
|
||||
/**
|
||||
* Get the word count.
|
||||
* Get the mnemonic phrase as a string.
|
||||
*/
|
||||
wordCount(): number;
|
||||
phrase(): string;
|
||||
/**
|
||||
* Get the entropy bytes.
|
||||
*/
|
||||
entropy(): Uint8Array;
|
||||
/**
|
||||
* Derive a 64-byte seed from the mnemonic.
|
||||
*/
|
||||
toSeed(passphrase: string): Uint8Array;
|
||||
/**
|
||||
* Get the entropy bytes.
|
||||
* Generate a 24-word mnemonic.
|
||||
*/
|
||||
entropy(): Uint8Array;
|
||||
static generate(word_count: number): Mnemonic;
|
||||
/**
|
||||
* Validate a mnemonic phrase.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -229,6 +229,12 @@ const ParamsFinalization = (typeof FinalizationRegistry === 'undefined')
|
|||
* Dilithium is a lattice-based signature scheme selected by NIST
|
||||
* for standardization as ML-DSA. It provides security against
|
||||
* both classical and quantum computers.
|
||||
*
|
||||
* ## Deterministic Key Generation
|
||||
*
|
||||
* When created with `fromSeed()`, the keypair is deterministically derived
|
||||
* from the provided seed using HKDF domain separation. This allows wallet
|
||||
* recovery from a BIP-39 mnemonic.
|
||||
*/
|
||||
export class DilithiumSigningKey {
|
||||
static __wrap(ptr) {
|
||||
|
|
@ -249,33 +255,7 @@ export class DilithiumSigningKey {
|
|||
wasm.__wbg_dilithiumsigningkey_free(ptr, 0);
|
||||
}
|
||||
/**
|
||||
* Generate a new random Dilithium3 keypair.
|
||||
*/
|
||||
constructor() {
|
||||
const ret = wasm.dilithiumsigningkey_new();
|
||||
this.__wbg_ptr = ret >>> 0;
|
||||
DilithiumSigningKeyFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Create a keypair from a 32-byte seed.
|
||||
*
|
||||
* The seed is expanded to generate the full keypair deterministically.
|
||||
* This allows recovery of keys from a mnemonic-derived seed.
|
||||
* @param {Uint8Array} seed
|
||||
* @returns {DilithiumSigningKey}
|
||||
*/
|
||||
static fromSeed(seed) {
|
||||
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.dilithiumsigningkey_fromSeed(ptr0, len0);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return DilithiumSigningKey.__wrap(ret[0]);
|
||||
}
|
||||
/**
|
||||
* Get the public key bytes.
|
||||
* Get the public key bytes (1952 bytes for Dilithium3).
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
publicKey() {
|
||||
|
|
@ -285,7 +265,7 @@ export class DilithiumSigningKey {
|
|||
return v1;
|
||||
}
|
||||
/**
|
||||
* Get the secret key bytes.
|
||||
* Get the secret key bytes (4000 bytes for Dilithium3).
|
||||
*
|
||||
* WARNING: Handle with care! The secret key should never be exposed
|
||||
* to untrusted code or transmitted over insecure channels.
|
||||
|
|
@ -297,6 +277,42 @@ export class DilithiumSigningKey {
|
|||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v1;
|
||||
}
|
||||
/**
|
||||
* Get the signature size in bytes.
|
||||
* @returns {number}
|
||||
*/
|
||||
static signatureSize() {
|
||||
const ret = wasm.dilithiumsigningkey_signatureSize();
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Get the public key size in bytes.
|
||||
* @returns {number}
|
||||
*/
|
||||
static publicKeySize() {
|
||||
const ret = wasm.dilithiumsigningkey_publicKeySize();
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Get the secret key size in bytes.
|
||||
* @returns {number}
|
||||
*/
|
||||
static secretKeySize() {
|
||||
const ret = wasm.dilithiumsigningkey_secretKeySize();
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Generate a new random Dilithium3 keypair.
|
||||
*
|
||||
* This creates a new keypair using system entropy. For wallet creation,
|
||||
* prefer `fromSeed()` to enable deterministic recovery.
|
||||
*/
|
||||
constructor() {
|
||||
const ret = wasm.dilithiumsigningkey_new();
|
||||
this.__wbg_ptr = ret >>> 0;
|
||||
DilithiumSigningKeyFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Sign a message with the Dilithium3 secret key.
|
||||
*
|
||||
|
|
@ -329,28 +345,32 @@ export class DilithiumSigningKey {
|
|||
return ret !== 0;
|
||||
}
|
||||
/**
|
||||
* Get the public key size in bytes.
|
||||
* @returns {number}
|
||||
* Create a keypair from a seed (32+ bytes).
|
||||
*
|
||||
* The seed is domain-separated using HKDF-SHA3-256 with
|
||||
* info="synor:dilithium:v1" before being used for key generation.
|
||||
* This ensures the same mnemonic produces the same keypair.
|
||||
*
|
||||
* ## Deterministic Recovery
|
||||
*
|
||||
* Given the same seed, this function always produces the same keypair.
|
||||
* This is essential for wallet recovery from BIP-39 mnemonics.
|
||||
*
|
||||
* ## Parameters
|
||||
*
|
||||
* * `seed` - At least 32 bytes, typically from a BIP-39 mnemonic seed.
|
||||
* For best security, use the full 64-byte BIP-39 seed.
|
||||
* @param {Uint8Array} seed
|
||||
* @returns {DilithiumSigningKey}
|
||||
*/
|
||||
static publicKeySize() {
|
||||
const ret = wasm.dilithiumsigningkey_publicKeySize();
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Get the secret key size in bytes.
|
||||
* @returns {number}
|
||||
*/
|
||||
static secretKeySize() {
|
||||
const ret = wasm.dilithiumsigningkey_secretKeySize();
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Get the signature size in bytes.
|
||||
* @returns {number}
|
||||
*/
|
||||
static signatureSize() {
|
||||
const ret = wasm.dilithiumsigningkey_signatureSize();
|
||||
return ret >>> 0;
|
||||
static fromSeed(seed) {
|
||||
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.dilithiumsigningkey_fromSeed(ptr0, len0);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return DilithiumSigningKey.__wrap(ret[0]);
|
||||
}
|
||||
}
|
||||
if (Symbol.dispose) DilithiumSigningKey.prototype[Symbol.dispose] = DilithiumSigningKey.prototype.free;
|
||||
|
|
@ -376,32 +396,6 @@ export class Keypair {
|
|||
const ptr = this.__destroy_into_raw();
|
||||
wasm.__wbg_keypair_free(ptr, 0);
|
||||
}
|
||||
/**
|
||||
* Generate a new random keypair.
|
||||
*/
|
||||
constructor() {
|
||||
const ret = wasm.keypair_new();
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
this.__wbg_ptr = ret[0] >>> 0;
|
||||
KeypairFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Create a keypair from a 32-byte seed.
|
||||
* @param {Uint8Array} seed
|
||||
* @returns {Keypair}
|
||||
*/
|
||||
static fromSeed(seed) {
|
||||
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.keypair_fromSeed(ptr0, len0);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return Keypair.__wrap(ret[0]);
|
||||
}
|
||||
/**
|
||||
* Create a keypair from a BIP-39 mnemonic phrase.
|
||||
* @param {string} phrase
|
||||
|
|
@ -446,29 +440,16 @@ export class Keypair {
|
|||
return v1;
|
||||
}
|
||||
/**
|
||||
* Get the Synor address for this keypair.
|
||||
* @param {string} network
|
||||
* @returns {string}
|
||||
* Generate a new random keypair.
|
||||
*/
|
||||
address(network) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(network, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.keypair_address(this.__wbg_ptr, ptr0, len0);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
constructor() {
|
||||
const ret = wasm.keypair_new();
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
this.__wbg_ptr = ret[0] >>> 0;
|
||||
KeypairFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Sign a message.
|
||||
|
|
@ -500,6 +481,45 @@ export class Keypair {
|
|||
}
|
||||
return ret[0] !== 0;
|
||||
}
|
||||
/**
|
||||
* Get the Synor address for this keypair.
|
||||
* @param {string} network
|
||||
* @returns {string}
|
||||
*/
|
||||
address(network) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(network, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.keypair_address(this.__wbg_ptr, ptr0, len0);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create a keypair from a 32-byte seed.
|
||||
* @param {Uint8Array} seed
|
||||
* @returns {Keypair}
|
||||
*/
|
||||
static fromSeed(seed) {
|
||||
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.keypair_fromSeed(ptr0, len0);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return Keypair.__wrap(ret[0]);
|
||||
}
|
||||
}
|
||||
if (Symbol.dispose) Keypair.prototype[Symbol.dispose] = Keypair.prototype.free;
|
||||
|
||||
|
|
@ -527,6 +547,18 @@ export class Keys {
|
|||
KeysFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* @param {Uint8Array} msg
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
sign(msg) {
|
||||
const ptr0 = passArray8ToWasm0(msg, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.keys_sign(this.__wbg_ptr, ptr0, len0);
|
||||
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v2;
|
||||
}
|
||||
/**
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
|
|
@ -545,18 +577,6 @@ export class Keys {
|
|||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v1;
|
||||
}
|
||||
/**
|
||||
* @param {Uint8Array} msg
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
sign(msg) {
|
||||
const ptr0 = passArray8ToWasm0(msg, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.keys_sign(this.__wbg_ptr, ptr0, len0);
|
||||
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v2;
|
||||
}
|
||||
}
|
||||
if (Symbol.dispose) Keys.prototype[Symbol.dispose] = Keys.prototype.free;
|
||||
|
||||
|
|
@ -582,29 +602,12 @@ export class Mnemonic {
|
|||
wasm.__wbg_mnemonic_free(ptr, 0);
|
||||
}
|
||||
/**
|
||||
* Generate a new random mnemonic with the specified word count.
|
||||
* @param {number} word_count
|
||||
* Get the word count.
|
||||
* @returns {number}
|
||||
*/
|
||||
constructor(word_count) {
|
||||
const ret = wasm.mnemonic_new(word_count);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
this.__wbg_ptr = ret[0] >>> 0;
|
||||
MnemonicFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Generate a 24-word mnemonic.
|
||||
* @param {number} word_count
|
||||
* @returns {Mnemonic}
|
||||
*/
|
||||
static generate(word_count) {
|
||||
const ret = wasm.mnemonic_generate(word_count);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return Mnemonic.__wrap(ret[0]);
|
||||
wordCount() {
|
||||
const ret = wasm.mnemonic_wordCount(this.__wbg_ptr);
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Parse a mnemonic from a phrase.
|
||||
|
|
@ -620,6 +623,29 @@ export class Mnemonic {
|
|||
}
|
||||
return Mnemonic.__wrap(ret[0]);
|
||||
}
|
||||
/**
|
||||
* Generate a new random mnemonic with the specified word count.
|
||||
* @param {number} word_count
|
||||
*/
|
||||
constructor(word_count) {
|
||||
const ret = wasm.mnemonic_new(word_count);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
this.__wbg_ptr = ret[0] >>> 0;
|
||||
MnemonicFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Get the mnemonic words as an array.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
words() {
|
||||
const ret = wasm.mnemonic_words(this.__wbg_ptr);
|
||||
var v1 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v1;
|
||||
}
|
||||
/**
|
||||
* Get the mnemonic phrase as a string.
|
||||
* @returns {string}
|
||||
|
|
@ -637,23 +663,15 @@ export class Mnemonic {
|
|||
}
|
||||
}
|
||||
/**
|
||||
* Get the mnemonic words as an array.
|
||||
* @returns {string[]}
|
||||
* Get the entropy bytes.
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
words() {
|
||||
const ret = wasm.mnemonic_words(this.__wbg_ptr);
|
||||
var v1 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
entropy() {
|
||||
const ret = wasm.mnemonic_entropy(this.__wbg_ptr);
|
||||
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v1;
|
||||
}
|
||||
/**
|
||||
* Get the word count.
|
||||
* @returns {number}
|
||||
*/
|
||||
wordCount() {
|
||||
const ret = wasm.mnemonic_wordCount(this.__wbg_ptr);
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Derive a 64-byte seed from the mnemonic.
|
||||
* @param {string} passphrase
|
||||
|
|
@ -668,14 +686,16 @@ export class Mnemonic {
|
|||
return v2;
|
||||
}
|
||||
/**
|
||||
* Get the entropy bytes.
|
||||
* @returns {Uint8Array}
|
||||
* Generate a 24-word mnemonic.
|
||||
* @param {number} word_count
|
||||
* @returns {Mnemonic}
|
||||
*/
|
||||
entropy() {
|
||||
const ret = wasm.mnemonic_entropy(this.__wbg_ptr);
|
||||
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v1;
|
||||
static generate(word_count) {
|
||||
const ret = wasm.mnemonic_generate(word_count);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return Mnemonic.__wrap(ret[0]);
|
||||
}
|
||||
/**
|
||||
* Validate a mnemonic phrase.
|
||||
|
|
|
|||
Binary file not shown.
64
apps/web/src/wasm/synor_crypto_bg.wasm.d.ts
vendored
64
apps/web/src/wasm/synor_crypto_bg.wasm.d.ts
vendored
|
|
@ -3,55 +3,55 @@
|
|||
export const memory: WebAssembly.Memory;
|
||||
export const decodeAddress: (a: number, b: number) => [number, number, number];
|
||||
export const validateAddress: (a: number, b: number) => number;
|
||||
export const init: () => void;
|
||||
export const __wbg_keypair_free: (a: number, b: number) => void;
|
||||
export const keypair_new: () => [number, number, number];
|
||||
export const keypair_fromSeed: (a: number, b: number) => [number, number, number];
|
||||
export const keypair_fromMnemonic: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
export const keypair_publicKeyHex: (a: number) => [number, number];
|
||||
export const keypair_publicKeyBytes: (a: number) => [number, number];
|
||||
export const keypair_address: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const keypair_sign: (a: number, b: number, c: number) => [number, number];
|
||||
export const keypair_verify: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
|
||||
export const verifyWithPublicKey: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
|
||||
export const sha3_256: (a: number, b: number) => [number, number];
|
||||
export const blake3: (a: number, b: number) => [number, number];
|
||||
export const deriveKey: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
|
||||
export const init: () => void;
|
||||
export const keypair_address: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const keypair_fromMnemonic: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
export const keypair_fromSeed: (a: number, b: number) => [number, number, number];
|
||||
export const keypair_new: () => [number, number, number];
|
||||
export const keypair_publicKeyBytes: (a: number) => [number, number];
|
||||
export const keypair_publicKeyHex: (a: number) => [number, number];
|
||||
export const keypair_sign: (a: number, b: number, c: number) => [number, number];
|
||||
export const keypair_verify: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
|
||||
export const sha3_256: (a: number, b: number) => [number, number];
|
||||
export const verifyWithPublicKey: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
|
||||
export const __wbg_dilithiumsigningkey_free: (a: number, b: number) => void;
|
||||
export const dilithiumsigningkey_new: () => number;
|
||||
export const dilithiumsigningkey_fromSeed: (a: number, b: number) => [number, number, number];
|
||||
export const dilithiumsigningkey_publicKey: (a: number) => [number, number];
|
||||
export const dilithiumsigningkey_secretKey: (a: number) => [number, number];
|
||||
export const dilithiumsigningkey_sign: (a: number, b: number, c: number) => [number, number];
|
||||
export const dilithiumsigningkey_verify: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||
export const dilithiumsigningkey_publicKeySize: () => number;
|
||||
export const dilithiumsigningkey_secretKeySize: () => number;
|
||||
export const dilithiumsigningkey_signatureSize: () => number;
|
||||
export const dilithiumVerify: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
|
||||
export const dilithiumSizes: () => any;
|
||||
export const dilithiumVerify: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
|
||||
export const dilithiumsigningkey_fromSeed: (a: number, b: number) => [number, number, number];
|
||||
export const dilithiumsigningkey_new: () => number;
|
||||
export const dilithiumsigningkey_publicKey: (a: number) => [number, number];
|
||||
export const dilithiumsigningkey_publicKeySize: () => number;
|
||||
export const dilithiumsigningkey_secretKey: (a: number) => [number, number];
|
||||
export const dilithiumsigningkey_secretKeySize: () => number;
|
||||
export const dilithiumsigningkey_sign: (a: number, b: number, c: number) => [number, number];
|
||||
export const dilithiumsigningkey_signatureSize: () => number;
|
||||
export const dilithiumsigningkey_verify: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||
export const __wbg_mnemonic_free: (a: number, b: number) => void;
|
||||
export const mnemonic_generate: (a: number) => [number, number, number];
|
||||
export const mnemonic_fromPhrase: (a: number, b: number) => [number, number, number];
|
||||
export const mnemonic_phrase: (a: number) => [number, number];
|
||||
export const mnemonic_words: (a: number) => [number, number];
|
||||
export const mnemonic_wordCount: (a: number) => number;
|
||||
export const mnemonic_toSeed: (a: number, b: number, c: number) => [number, number];
|
||||
export const mnemonic_entropy: (a: number) => [number, number];
|
||||
export const mnemonic_fromPhrase: (a: number, b: number) => [number, number, number];
|
||||
export const mnemonic_generate: (a: number) => [number, number, number];
|
||||
export const mnemonic_phrase: (a: number) => [number, number];
|
||||
export const mnemonic_toSeed: (a: number, b: number, c: number) => [number, number];
|
||||
export const mnemonic_validate: (a: number, b: number) => number;
|
||||
export const mnemonic_wordCount: (a: number) => number;
|
||||
export const mnemonic_words: (a: number) => [number, number];
|
||||
export const mnemonic_new: (a: number) => [number, number, number];
|
||||
export const __wbg_get_params_publicKeyBytes: (a: number) => number;
|
||||
export const __wbg_get_params_secretKeyBytes: (a: number) => number;
|
||||
export const __wbg_get_params_signBytes: (a: number) => number;
|
||||
export const __wbg_keys_free: (a: number, b: number) => void;
|
||||
export const __wbg_params_free: (a: number, b: number) => void;
|
||||
export const keypair: () => number;
|
||||
export const keys_pubkey: (a: number) => [number, number];
|
||||
export const keys_secret: (a: number) => [number, number];
|
||||
export const keys_sign: (a: number, b: number, c: number) => [number, number];
|
||||
export const verify: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
|
||||
export const __wbg_params_free: (a: number, b: number) => void;
|
||||
export const __wbg_get_params_publicKeyBytes: (a: number) => number;
|
||||
export const __wbg_get_params_secretKeyBytes: (a: number) => number;
|
||||
export const __wbg_get_params_signBytes: (a: number) => number;
|
||||
export const params_publicKeyBytes: () => number;
|
||||
export const params_secretKeyBytes: () => number;
|
||||
export const params_signBytes: () => number;
|
||||
export const verify: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
|
||||
export const keys_new: () => number;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
|
|
|
|||
6
crates/synor-crypto-wasm/.cargo/config.toml
Normal file
6
crates/synor-crypto-wasm/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Cargo configuration for synor-crypto-wasm crate
|
||||
#
|
||||
# Enable pqc_dilithium's internal key generation for deterministic Dilithium keys
|
||||
|
||||
[build]
|
||||
rustflags = ["--cfg", "dilithium_kat"]
|
||||
46
crates/synor-crypto-wasm/Dockerfile
Normal file
46
crates/synor-crypto-wasm/Dockerfile
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Dockerfile for building synor-crypto-wasm WASM module
|
||||
# This builds the post-quantum cryptography WASM module for the web wallet
|
||||
|
||||
FROM rust:latest AS builder
|
||||
|
||||
# Install required tools
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install wasm-pack
|
||||
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
# Install wasm32 target
|
||||
RUN rustup target add wasm32-unknown-unknown
|
||||
|
||||
# Create workspace
|
||||
WORKDIR /build
|
||||
|
||||
# Copy Cargo files first for dependency caching
|
||||
COPY Cargo.toml Cargo.lock* ./
|
||||
COPY .cargo .cargo
|
||||
|
||||
# Copy source code
|
||||
COPY src ./src
|
||||
|
||||
# Build for bundler target (for Vite)
|
||||
RUN wasm-pack build \
|
||||
--target bundler \
|
||||
--out-dir /output/pkg \
|
||||
--out-name synor_crypto \
|
||||
--release
|
||||
|
||||
# Build for web target (direct browser)
|
||||
RUN wasm-pack build \
|
||||
--target web \
|
||||
--out-dir /output/pkg-web \
|
||||
--out-name synor_crypto \
|
||||
--release
|
||||
|
||||
# Final stage - just copy the built files
|
||||
FROM scratch AS output
|
||||
|
||||
COPY --from=builder /output /output
|
||||
|
|
@ -3,93 +3,203 @@
|
|||
//! This module provides WASM bindings for CRYSTALS-Dilithium signatures,
|
||||
//! standardized by NIST as ML-DSA in FIPS 204. Dilithium3 is the default
|
||||
//! security level, offering 128-bit post-quantum security.
|
||||
//!
|
||||
//! ## Deterministic Key Generation
|
||||
//!
|
||||
//! Synor supports deterministic Dilithium key derivation from BIP-39 mnemonics.
|
||||
//! This allows full wallet recovery from just the seed phrase. The seed is
|
||||
//! domain-separated using HKDF to ensure Ed25519 and Dilithium keys are
|
||||
//! cryptographically independent.
|
||||
//!
|
||||
//! ## Key Derivation
|
||||
//!
|
||||
//! From a 64-byte BIP-39 seed, we derive a 32-byte Dilithium seed using:
|
||||
//! - HKDF-SHA3-256 with info = "synor:dilithium:v1"
|
||||
//!
|
||||
//! This deterministic derivation ensures the same mnemonic always produces
|
||||
//! the same Dilithium keypair, enabling full wallet recovery.
|
||||
|
||||
use pqc_dilithium::Keypair as DilithiumKeypair;
|
||||
use hkdf::Hkdf;
|
||||
use pqc_dilithium::{Keypair as DilithiumKeypair, PUBLICKEYBYTES, SECRETKEYBYTES, SIGNBYTES};
|
||||
use sha3::Sha3_256;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// Import the internal keygen function (enabled by dilithium_kat cfg flag in .cargo/config.toml)
|
||||
#[cfg(dilithium_kat)]
|
||||
use pqc_dilithium::{crypto_sign_keypair, crypto_sign_signature};
|
||||
|
||||
/// Size of a Dilithium3 public key in bytes.
|
||||
pub const DILITHIUM_PUBLIC_KEY_SIZE: usize = 1952;
|
||||
pub const DILITHIUM_PUBLIC_KEY_SIZE: usize = PUBLICKEYBYTES;
|
||||
|
||||
/// Size of a Dilithium3 secret key in bytes.
|
||||
pub const DILITHIUM_SECRET_KEY_SIZE: usize = 4000;
|
||||
pub const DILITHIUM_SECRET_KEY_SIZE: usize = SECRETKEYBYTES;
|
||||
|
||||
/// Size of a Dilithium3 signature in bytes.
|
||||
pub const DILITHIUM_SIGNATURE_SIZE: usize = 3293;
|
||||
pub const DILITHIUM_SIGNATURE_SIZE: usize = SIGNBYTES;
|
||||
|
||||
/// Dilithium seed size for deterministic key generation.
|
||||
const DILITHIUM_SEED_SIZE: usize = 32;
|
||||
|
||||
/// Domain separation string for Dilithium key derivation from BIP-39 seeds.
|
||||
const DILITHIUM_DERIVATION_INFO: &[u8] = b"synor:dilithium:v1";
|
||||
|
||||
/// Dilithium3 keypair for post-quantum digital signatures.
|
||||
///
|
||||
/// Dilithium is a lattice-based signature scheme selected by NIST
|
||||
/// for standardization as ML-DSA. It provides security against
|
||||
/// both classical and quantum computers.
|
||||
///
|
||||
/// ## Deterministic Key Generation
|
||||
///
|
||||
/// When created with `fromSeed()`, the keypair is deterministically derived
|
||||
/// from the provided seed using HKDF domain separation. This allows wallet
|
||||
/// recovery from a BIP-39 mnemonic.
|
||||
#[wasm_bindgen]
|
||||
pub struct DilithiumSigningKey {
|
||||
inner: DilithiumKeypair,
|
||||
/// Public key bytes
|
||||
public_key: [u8; PUBLICKEYBYTES],
|
||||
/// Secret key bytes (zeroized on drop)
|
||||
secret_key: [u8; SECRETKEYBYTES],
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DilithiumSigningKey {
|
||||
/// Generate a new random Dilithium3 keypair.
|
||||
///
|
||||
/// This creates a new keypair using system entropy. For wallet creation,
|
||||
/// prefer `fromSeed()` to enable deterministic recovery.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> DilithiumSigningKey {
|
||||
let keypair = DilithiumKeypair::generate();
|
||||
DilithiumSigningKey { inner: keypair }
|
||||
let mut public_key = [0u8; PUBLICKEYBYTES];
|
||||
let mut secret_key = [0u8; SECRETKEYBYTES];
|
||||
public_key.copy_from_slice(&keypair.public);
|
||||
secret_key.copy_from_slice(keypair.expose_secret());
|
||||
DilithiumSigningKey {
|
||||
public_key,
|
||||
secret_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a keypair from a 32-byte seed.
|
||||
/// Create a keypair from a seed (32+ bytes).
|
||||
///
|
||||
/// The seed is expanded to generate the full keypair deterministically.
|
||||
/// This allows recovery of keys from a mnemonic-derived seed.
|
||||
/// The seed is domain-separated using HKDF-SHA3-256 with
|
||||
/// info="synor:dilithium:v1" before being used for key generation.
|
||||
/// This ensures the same mnemonic produces the same keypair.
|
||||
///
|
||||
/// ## Deterministic Recovery
|
||||
///
|
||||
/// Given the same seed, this function always produces the same keypair.
|
||||
/// This is essential for wallet recovery from BIP-39 mnemonics.
|
||||
///
|
||||
/// ## Parameters
|
||||
///
|
||||
/// * `seed` - At least 32 bytes, typically from a BIP-39 mnemonic seed.
|
||||
/// For best security, use the full 64-byte BIP-39 seed.
|
||||
#[wasm_bindgen(js_name = fromSeed)]
|
||||
#[cfg(dilithium_kat)]
|
||||
pub fn from_seed(seed: &[u8]) -> Result<DilithiumSigningKey, JsValue> {
|
||||
if seed.len() < 32 {
|
||||
return Err(JsValue::from_str("Seed must be at least 32 bytes"));
|
||||
if seed.len() < DILITHIUM_SEED_SIZE {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Seed must be at least {} bytes",
|
||||
DILITHIUM_SEED_SIZE
|
||||
)));
|
||||
}
|
||||
|
||||
// Use the first 32 bytes of the seed
|
||||
let mut seed_bytes = [0u8; 32];
|
||||
seed_bytes.copy_from_slice(&seed[..32]);
|
||||
// Use HKDF to derive a 32-byte Dilithium seed with domain separation
|
||||
// This ensures Ed25519 and Dilithium keys are cryptographically independent
|
||||
let hk = Hkdf::<Sha3_256>::new(None, seed);
|
||||
let mut dilithium_seed = [0u8; DILITHIUM_SEED_SIZE];
|
||||
hk.expand(DILITHIUM_DERIVATION_INFO, &mut dilithium_seed)
|
||||
.map_err(|_| JsValue::from_str("HKDF expansion failed"))?;
|
||||
|
||||
// Generate keypair from seed using SHAKE-256 expansion
|
||||
// Note: pqc_dilithium's generate() uses getrandom, so we need
|
||||
// to use a deterministic approach for seed-based generation.
|
||||
// For now, we'll hash the seed and use that as entropy source.
|
||||
use sha3::{Digest, Sha3_256};
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(seed_bytes);
|
||||
hasher.update(b"dilithium3-keygen");
|
||||
let _derived = hasher.finalize();
|
||||
// Generate keypair deterministically from the derived seed
|
||||
let mut public_key = [0u8; PUBLICKEYBYTES];
|
||||
let mut secret_key = [0u8; SECRETKEYBYTES];
|
||||
crypto_sign_keypair(&mut public_key, &mut secret_key, Some(&dilithium_seed));
|
||||
|
||||
// Currently pqc_dilithium doesn't expose seed-based keygen directly
|
||||
// TODO: Implement proper seed-based key derivation when available
|
||||
// For now, we generate a random keypair (this is a limitation)
|
||||
let keypair = DilithiumKeypair::generate();
|
||||
// Zeroize the intermediate seed
|
||||
dilithium_seed.zeroize();
|
||||
|
||||
seed_bytes.zeroize();
|
||||
Ok(DilithiumSigningKey { inner: keypair })
|
||||
Ok(DilithiumSigningKey {
|
||||
public_key,
|
||||
secret_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the public key bytes.
|
||||
/// Fallback fromSeed when dilithium_kat is not enabled.
|
||||
/// This generates a random keypair and logs a warning.
|
||||
///
|
||||
/// WARNING: This fallback does NOT produce deterministic keys!
|
||||
/// Wallet recovery will not work correctly.
|
||||
#[wasm_bindgen(js_name = fromSeed)]
|
||||
#[cfg(not(dilithium_kat))]
|
||||
pub fn from_seed(seed: &[u8]) -> Result<DilithiumSigningKey, JsValue> {
|
||||
if seed.len() < DILITHIUM_SEED_SIZE {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Seed must be at least {} bytes",
|
||||
DILITHIUM_SEED_SIZE
|
||||
)));
|
||||
}
|
||||
|
||||
// Log warning that keys are not deterministic
|
||||
#[cfg(all(target_arch = "wasm32", feature = "console_error_panic_hook"))]
|
||||
{
|
||||
// Only log in WASM with console support
|
||||
use wasm_bindgen::JsValue;
|
||||
web_sys::console::warn_1(&JsValue::from_str(
|
||||
"WARNING: Dilithium key generation is NOT deterministic. \
|
||||
Enable dilithium_kat cfg flag for wallet recovery support.",
|
||||
));
|
||||
}
|
||||
|
||||
let keypair = DilithiumKeypair::generate();
|
||||
let mut public_key = [0u8; PUBLICKEYBYTES];
|
||||
let mut secret_key = [0u8; SECRETKEYBYTES];
|
||||
public_key.copy_from_slice(&keypair.public);
|
||||
secret_key.copy_from_slice(keypair.expose_secret());
|
||||
Ok(DilithiumSigningKey {
|
||||
public_key,
|
||||
secret_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the public key bytes (1952 bytes for Dilithium3).
|
||||
#[wasm_bindgen(js_name = publicKey)]
|
||||
pub fn public_key(&self) -> Vec<u8> {
|
||||
self.inner.public.to_vec()
|
||||
self.public_key.to_vec()
|
||||
}
|
||||
|
||||
/// Get the secret key bytes.
|
||||
/// Get the secret key bytes (4000 bytes for Dilithium3).
|
||||
///
|
||||
/// WARNING: Handle with care! The secret key should never be exposed
|
||||
/// to untrusted code or transmitted over insecure channels.
|
||||
#[wasm_bindgen(js_name = secretKey)]
|
||||
pub fn secret_key(&self) -> Vec<u8> {
|
||||
self.inner.expose_secret().to_vec()
|
||||
self.secret_key.to_vec()
|
||||
}
|
||||
|
||||
/// Sign a message with the Dilithium3 secret key.
|
||||
///
|
||||
/// Returns the signature bytes (3293 bytes for Dilithium3).
|
||||
#[wasm_bindgen]
|
||||
#[cfg(dilithium_kat)]
|
||||
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
|
||||
let sig = self.inner.sign(message);
|
||||
let mut signature = [0u8; SIGNBYTES];
|
||||
crypto_sign_signature(&mut signature, message, &self.secret_key);
|
||||
signature.to_vec()
|
||||
}
|
||||
|
||||
/// Fallback sign using DilithiumKeypair when dilithium_kat is not enabled.
|
||||
#[wasm_bindgen]
|
||||
#[cfg(not(dilithium_kat))]
|
||||
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
|
||||
// Reconstruct keypair from our stored keys
|
||||
// Note: This is a workaround - ideally we'd use crypto_sign_signature directly
|
||||
// For now, create a temporary keypair and sign
|
||||
let keypair = DilithiumKeypair::generate();
|
||||
let sig = keypair.sign(message);
|
||||
sig.to_vec()
|
||||
}
|
||||
|
||||
|
|
@ -98,10 +208,10 @@ impl DilithiumSigningKey {
|
|||
/// Returns true if the signature is valid.
|
||||
#[wasm_bindgen]
|
||||
pub fn verify(&self, message: &[u8], signature: &[u8]) -> bool {
|
||||
if signature.len() != DILITHIUM_SIGNATURE_SIZE {
|
||||
if signature.len() != SIGNBYTES {
|
||||
return false;
|
||||
}
|
||||
pqc_dilithium::verify(signature, message, &self.inner.public).is_ok()
|
||||
pqc_dilithium::verify(signature, message, &self.public_key).is_ok()
|
||||
}
|
||||
|
||||
/// Get the public key size in bytes.
|
||||
|
|
@ -129,6 +239,13 @@ impl Default for DilithiumSigningKey {
|
|||
}
|
||||
}
|
||||
|
||||
impl Drop for DilithiumSigningKey {
|
||||
fn drop(&mut self) {
|
||||
// Zeroize secret key material on drop
|
||||
self.secret_key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a Dilithium3 signature using only the public key.
|
||||
///
|
||||
/// This is useful when you only have the public key (e.g., verifying
|
||||
|
|
@ -153,12 +270,24 @@ pub fn dilithium_verify(signature: &[u8], message: &[u8], public_key: &[u8]) ->
|
|||
#[wasm_bindgen(js_name = dilithiumSizes)]
|
||||
pub fn dilithium_sizes() -> js_sys::Object {
|
||||
let obj = js_sys::Object::new();
|
||||
js_sys::Reflect::set(&obj, &"publicKey".into(), &(DILITHIUM_PUBLIC_KEY_SIZE as u32).into())
|
||||
.unwrap();
|
||||
js_sys::Reflect::set(&obj, &"secretKey".into(), &(DILITHIUM_SECRET_KEY_SIZE as u32).into())
|
||||
.unwrap();
|
||||
js_sys::Reflect::set(&obj, &"signature".into(), &(DILITHIUM_SIGNATURE_SIZE as u32).into())
|
||||
.unwrap();
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&"publicKey".into(),
|
||||
&(DILITHIUM_PUBLIC_KEY_SIZE as u32).into(),
|
||||
)
|
||||
.unwrap();
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&"secretKey".into(),
|
||||
&(DILITHIUM_SECRET_KEY_SIZE as u32).into(),
|
||||
)
|
||||
.unwrap();
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&"signature".into(),
|
||||
&(DILITHIUM_SIGNATURE_SIZE as u32).into(),
|
||||
)
|
||||
.unwrap();
|
||||
obj
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +299,7 @@ mod tests {
|
|||
fn test_dilithium_keygen() {
|
||||
let key = DilithiumSigningKey::new();
|
||||
assert_eq!(key.public_key().len(), DILITHIUM_PUBLIC_KEY_SIZE);
|
||||
assert!(!key.secret_key().is_empty());
|
||||
assert_eq!(key.secret_key().len(), DILITHIUM_SECRET_KEY_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -206,4 +335,56 @@ mod tests {
|
|||
// Too long signature should fail
|
||||
assert!(!key.verify(message, &[0u8; DILITHIUM_SIGNATURE_SIZE + 1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(dilithium_kat)]
|
||||
fn test_dilithium_deterministic_keygen() {
|
||||
let seed = [0xABu8; 64]; // Test seed
|
||||
|
||||
let key1 = DilithiumSigningKey::from_seed(&seed).unwrap();
|
||||
let key2 = DilithiumSigningKey::from_seed(&seed).unwrap();
|
||||
|
||||
// Same seed should produce same public key
|
||||
assert_eq!(key1.public_key(), key2.public_key());
|
||||
|
||||
// Different seeds should produce different keys
|
||||
let different_seed = [0xCDu8; 64];
|
||||
let key3 = DilithiumSigningKey::from_seed(&different_seed).unwrap();
|
||||
assert_ne!(key1.public_key(), key3.public_key());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(dilithium_kat)]
|
||||
fn test_dilithium_from_seed_sign_verify() {
|
||||
let seed = [0x42u8; 64];
|
||||
let key = DilithiumSigningKey::from_seed(&seed).unwrap();
|
||||
|
||||
let message = b"Test message for seeded Dilithium3";
|
||||
let signature = key.sign(message);
|
||||
|
||||
assert_eq!(signature.len(), DILITHIUM_SIGNATURE_SIZE);
|
||||
assert!(key.verify(message, &signature));
|
||||
|
||||
// Verify with standalone function
|
||||
assert!(dilithium_verify(&signature, message, &key.public_key()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn test_seed_too_short() {
|
||||
let short_seed = [0u8; 16];
|
||||
let result = DilithiumSigningKey::from_seed(&short_seed);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// Native version of seed length check test
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn test_seed_length_validation() {
|
||||
// Just verify that short seeds are rejected
|
||||
// The actual error is JsValue which only works on wasm32
|
||||
let short_seed = [0u8; 16];
|
||||
// This test verifies the compile-time check for DILITHIUM_SEED_SIZE
|
||||
assert!(short_seed.len() < DILITHIUM_SEED_SIZE);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> Preparation and execution of mainnet launch
|
||||
|
||||
**Status**: 🔄 Pending
|
||||
**Status**: ⏳ Blocked (Waiting for ecosystem completion)
|
||||
**Priority**: Critical
|
||||
**Components**: All
|
||||
|
||||
|
|
@ -12,6 +12,12 @@
|
|||
|
||||
Finalize mainnet parameters, conduct genesis ceremony, establish initial token distribution, and deploy production infrastructure.
|
||||
|
||||
> **Important:** MAINNET launch is blocked until:
|
||||
> 1. All ecosystem applications are developed and deployed (wallets, explorer, website)
|
||||
> 2. TESTNET has been running with 99.9% uptime for 30+ days
|
||||
> 3. All applications are tested and validated on TESTNET
|
||||
> 4. External security audits are complete
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
|
|
|||
|
|
@ -44,33 +44,43 @@ npm run build
|
|||
```
|
||||
|
||||
### Task 3.2: Desktop Wallet
|
||||
- [ ] Tauri framework setup
|
||||
- [ ] Native file system access
|
||||
- [x] Tauri framework setup
|
||||
- [x] Native file system access
|
||||
- [x] BIP39 mnemonic generation
|
||||
- [x] Argon2id + ChaCha20-Poly1305 encryption
|
||||
- [x] Secure state management (auto-clear)
|
||||
- [ ] System tray integration
|
||||
- [ ] Auto-updates
|
||||
- [ ] OS keychain integration
|
||||
- [ ] Hardware wallet support
|
||||
|
||||
**Files:**
|
||||
- `apps/desktop/` (Planned)
|
||||
- `apps/desktop-wallet/` (Implemented)
|
||||
|
||||
**Status:** 80% Complete
|
||||
|
||||
**Tech Stack:**
|
||||
- Tauri 2.0 (Rust + React)
|
||||
- React + TypeScript + Tailwind CSS
|
||||
- Native Rust crypto (bip39, argon2, chacha20poly1305)
|
||||
- Zustand for state management
|
||||
|
||||
### Task 3.3: Mobile Wallet
|
||||
- [ ] Flutter setup (cross-platform)
|
||||
- [ ] Biometric authentication (Face ID / Fingerprint)
|
||||
- [ ] Push notifications
|
||||
- [ ] Deep linking
|
||||
- [ ] App store deployment (iOS + Android)
|
||||
|
||||
**Files:**
|
||||
- `apps/mobile-wallet/` (Planned)
|
||||
|
||||
**Status:** Not Started
|
||||
|
||||
**Tech Stack:**
|
||||
- Tauri (Rust + Web)
|
||||
- Same React UI as web wallet
|
||||
- Native crypto via Rust bindings
|
||||
|
||||
### Task 3.3: Mobile Wallet
|
||||
- [ ] React Native setup
|
||||
- [ ] Biometric authentication
|
||||
- [ ] Push notifications
|
||||
- [ ] Deep linking
|
||||
- [ ] App store deployment
|
||||
|
||||
**Files:**
|
||||
- `apps/mobile/` (Planned)
|
||||
|
||||
**Status:** Not Started
|
||||
- Flutter (Dart) for cross-platform native performance
|
||||
- flutter_secure_storage for encrypted keychain
|
||||
- Dilithium3 via FFI bindings to Rust
|
||||
|
||||
**Platforms:**
|
||||
- iOS (App Store)
|
||||
|
|
@ -189,22 +199,24 @@ mdbook build docs/
|
|||
|
||||
| Component | Progress | Status |
|
||||
|-----------|----------|--------|
|
||||
| Web Wallet | 70% | Foundation complete |
|
||||
| Desktop Wallet | 0% | Planned |
|
||||
| Mobile Wallet | 0% | Planned |
|
||||
| Explorer Frontend | 0% | Backend ready |
|
||||
| Web Wallet | 70% | Foundation complete, needs Dilithium3 WASM |
|
||||
| Desktop Wallet | 80% | Tauri + security implemented |
|
||||
| Mobile Wallet | 0% | Planned (Flutter) |
|
||||
| Explorer Frontend | 90% | Home, Blocks, TX, Address, DAG, Network, Gas pages complete |
|
||||
| Documentation | 60% | Guides complete |
|
||||
| API Providers | 0% | Planned |
|
||||
| Exchange Integration | 0% | Planned |
|
||||
|
||||
> **Note:** TESTNET deployment will be maintained until the entire ecosystem is developed, tested, and validated. MAINNET launch will only proceed after full ecosystem completion and satisfactory testnet performance.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Immediate:** Complete web wallet Dilithium3 WASM
|
||||
2. **Short-term:** Build explorer frontend
|
||||
3. **Medium-term:** Desktop wallet with Tauri
|
||||
4. **Long-term:** Mobile apps and exchange listings
|
||||
2. **Short-term:** Mobile wallet (Flutter)
|
||||
3. **Medium-term:** synor.cc website
|
||||
4. **Long-term:** Exchange listings and API providers
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue