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
JavaScript
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');
}
}