UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

192 lines (191 loc) 8.25 kB
import { PublicKey } from '../../bitcore/publickey.js'; export const DEFAULT_MUSIG2_SECURITY = { minSigners: 2, maxSigners: 15, maxSessionDuration: 10 * 60 * 1000, requireValidPublicKeys: true, maxMessageSize: 100_000, maxTimestampSkew: 5 * 60 * 1000, maxInvalidMessagesPerPeer: 10, enableValidationSecurity: true, trackValidationViolations: true, }; export class MuSig2SecurityValidator { config; validationViolations = new Map(); blockedPeers = new Set(); constructor(config = DEFAULT_MUSIG2_SECURITY) { this.config = config; } async validateResourceAnnouncement(resourceType, resourceId, data, peerId) { if (!resourceType.startsWith('musig2-session')) { return true; } if (!data || typeof data !== 'object') { console.warn('[MuSig2Security] Invalid announcement data type'); return false; } const announcement = data; if (!announcement.sessionId || !announcement.coordinatorPeerId) { console.warn('[MuSig2Security] Missing required announcement fields'); return false; } if (announcement.sessionId !== resourceId) { console.warn('[MuSig2Security] Session ID mismatch'); return false; } const minSigners = this.config.minSigners ?? DEFAULT_MUSIG2_SECURITY.minSigners; const maxSigners = this.config.maxSigners ?? DEFAULT_MUSIG2_SECURITY.maxSigners; if (announcement.requiredSigners < minSigners) { console.warn(`[MuSig2Security] Too few signers: ${announcement.requiredSigners} < ${minSigners}`); return false; } if (announcement.requiredSigners > maxSigners) { console.warn(`[MuSig2Security] Too many signers: ${announcement.requiredSigners} > ${maxSigners}`); return false; } if (announcement.signers) { if (announcement.signers.length !== announcement.requiredSigners) { console.warn('[MuSig2Security] Signer count mismatch'); return false; } if (this.config.requireValidPublicKeys ?? DEFAULT_MUSIG2_SECURITY.requireValidPublicKeys) { for (const signerPubKey of announcement.signers) { try { PublicKey.fromString(signerPubKey); } catch (error) { console.warn(`[MuSig2Security] Invalid signer public key: ${signerPubKey}`); return false; } } } } const now = Date.now(); if (announcement.createdAt > now + 60000) { console.warn('[MuSig2Security] Announcement timestamp in future'); return false; } if (announcement.expiresAt < now) { console.warn('[MuSig2Security] Announcement expired'); return false; } const maxDuration = this.config.maxSessionDuration ?? DEFAULT_MUSIG2_SECURITY.maxSessionDuration; if (announcement.expiresAt - announcement.createdAt > maxDuration) { console.warn('[MuSig2Security] Announcement duration too long'); return false; } if (!/^[0-9a-f]{64}$/i.test(announcement.messageHash)) { console.warn('[MuSig2Security] Invalid message hash format'); return false; } return true; } async validateMessage(message, from) { if (this.blockedPeers.has(from.peerId)) { console.warn(`[MuSig2Security] Blocked peer attempted message: ${from.peerId}`); return false; } const maxMessageSize = this.config.maxMessageSize ?? DEFAULT_MUSIG2_SECURITY.maxMessageSize; if (this._isMessageTooLarge(message, maxMessageSize)) { this._trackValidationViolation(from.peerId, 'message_too_large'); return false; } if (!message.payload || typeof message.payload !== 'object') { console.warn('[MuSig2Security] Invalid message payload'); this._trackValidationViolation(from.peerId, 'invalid_payload'); return false; } const payload = message.payload; if (!payload.sessionId || !payload.timestamp) { console.warn('[MuSig2Security] Missing sessionId or timestamp'); this._trackValidationViolation(from.peerId, 'missing_fields'); return false; } const maxTimestampSkew = this.config.maxTimestampSkew ?? DEFAULT_MUSIG2_SECURITY.maxTimestampSkew; const now = Date.now(); const messageTime = payload.timestamp; if (Math.abs(now - messageTime) > maxTimestampSkew) { console.warn('[MuSig2Security] Message timestamp too old or in future'); this._trackValidationViolation(from.peerId, 'timestamp_skew'); return false; } return true; } _isMessageTooLarge(message, maxSize) { try { const serialized = JSON.stringify(message); return serialized.length > maxSize; } catch { return true; } } _trackValidationViolation(peerId, violationType) { const trackViolations = this.config.trackValidationViolations ?? DEFAULT_MUSIG2_SECURITY.trackValidationViolations; if (!trackViolations) { return; } const currentCount = this.validationViolations.get(peerId) ?? 0; const newCount = currentCount + 1; this.validationViolations.set(peerId, newCount); console.warn(`[MuSig2Security] Validation violation from ${peerId}: ${violationType} (count: ${newCount})`); const maxInvalidMessages = this.config.maxInvalidMessagesPerPeer ?? DEFAULT_MUSIG2_SECURITY.maxInvalidMessagesPerPeer; if (newCount >= maxInvalidMessages) { this.blockedPeers.add(peerId); console.warn(`[MuSig2Security] Blocked peer ${peerId} due to ${newCount} validation violations`); } } async canAnnounceResource(resourceType, peerId) { if (this.blockedPeers.has(peerId)) { console.warn(`[MuSig2Security] Blocked peer ${peerId} attempted to announce resource`); return false; } if (!resourceType.startsWith('musig2-session')) { return true; } return true; } getSecurityStatus() { return { blockedPeers: Array.from(this.blockedPeers), blockedPeerCount: this.blockedPeers.size, validationViolations: Object.fromEntries(this.validationViolations), totalViolations: Array.from(this.validationViolations.values()).reduce((a, b) => a + b, 0), config: { maxMessageSize: this.config.maxMessageSize ?? DEFAULT_MUSIG2_SECURITY.maxMessageSize, maxTimestampSkew: this.config.maxTimestampSkew ?? DEFAULT_MUSIG2_SECURITY.maxTimestampSkew, maxInvalidMessagesPerPeer: this.config.maxInvalidMessagesPerPeer ?? DEFAULT_MUSIG2_SECURITY.maxInvalidMessagesPerPeer, enableValidationSecurity: this.config.enableValidationSecurity ?? DEFAULT_MUSIG2_SECURITY.enableValidationSecurity, trackValidationViolations: this.config.trackValidationViolations ?? DEFAULT_MUSIG2_SECURITY.trackValidationViolations, }, }; } isPeerBlocked(peerId) { return this.blockedPeers.has(peerId); } getViolationCount(peerId) { return this.validationViolations.get(peerId) ?? 0; } unblockPeer(peerId) { const wasBlocked = this.blockedPeers.delete(peerId); if (wasBlocked) { this.validationViolations.delete(peerId); console.log(`[MuSig2Security] Unblocked peer: ${peerId}`); } return wasBlocked; } clearViolations() { this.validationViolations.clear(); this.blockedPeers.clear(); console.log('[MuSig2Security] Cleared all violations and blocked peers'); } }